1 /******************************************************************************
2  *
3  *  SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *
7  *******************************************************************************/
8 
9 //
10 // This class is a rather huge monster. It's something that resembles a QAbstractItemModel
11 // (because it has to provide the interface for a QTreeView) but isn't entirely one
12 // (for optimization reasons). It basically manages a tree of items of two types:
13 // GroupHeaderItem and MessageItem. Be sure to read the docs for ViewItemJob.
14 //
15 // A huge credit here goes to Till Adam which seems to have written most
16 // (if not all) of the original KMail threading code. The KMHeaders implementation,
17 // the documentation and his clever ideas were my starting points and essential tools.
18 // This is why I'm adding his copyright entry (copied from headeritem.cpp) here even if
19 // he didn't write a byte in this file until now :)
20 //
21 //                                       Szymon Tomasz Stefanek, 03 Aug 2008 04:50 (am)
22 //
23 // This class contains ideas from:
24 //
25 //   kmheaders.cpp / kmheaders.h, headeritem.cpp / headeritem.h
26 //   Copyright: (c) 2004 Till Adam < adam at kde dot org >
27 //
28 #include "core/model.h"
29 #include "core/delegate.h"
30 #include "core/filter.h"
31 #include "core/groupheaderitem.h"
32 #include "core/item_p.h"
33 #include "core/manager.h"
34 #include "core/messageitem.h"
35 #include "core/messageitemsetmanager.h"
36 #include "core/model_p.h"
37 #include "core/modelinvariantrowmapper.h"
38 #include "core/storagemodelbase.h"
39 #include "core/theme.h"
40 #include "core/view.h"
41 #include "messagelist_debug.h"
42 #include <config-messagelist.h>
43 
44 #include "MessageCore/StringUtil"
45 #include <Akonadi/Item>
46 #include <Akonadi/KMime/MessageStatus>
47 
48 #include <KLocalizedString>
49 
50 #include <QApplication>
51 #include <QDateTime>
52 #include <QElapsedTimer>
53 #include <QIcon>
54 #include <QLocale>
55 #include <QScrollBar>
56 #include <QTimer>
57 
58 #include <algorithm>
59 #include <chrono>
60 
61 using namespace std::chrono_literals;
62 
63 namespace MessageList
64 {
65 namespace Core
66 {
67 Q_GLOBAL_STATIC(QTimer, _k_heartBeatTimer)
68 
69 /**
70  * A job in a "View Fill" or "View Cleanup" or "View Update" task.
71  *
72  * For a "View Fill" task a job is a set of messages
73  * that are contiguous in the storage. The set is expressed as a range
74  * of row indexes. The task "sweeps" the storage in the specified
75  * range, creates the appropriate Item instances and places them
76  * in the right position in the tree.
77  *
78  * The idea is that in a single instance and for the same StorageModel
79  * the jobs should never "cover" the same message twice. This assertion
80  * is enforced all around this source file.
81  *
82  * For a "View Cleanup" task the job is a list of ModelInvariantIndex
83  * objects (that are in fact MessageItem objects) that need to be removed
84  * from the view.
85  *
86  * For a "View Update" task the job is a list of ModelInvariantIndex
87  * objects (that are in fact MessageItem objects) that need to be updated.
88  *
89  * The interesting fact is that all the tasks need
90  * very similar operations to be performed on the message tree.
91  *
92  * For a "View Fill" we have 5 passes.
93  *
94  * Pass 1 scans the underlying storage, creates the MessageItem objects
95  * (which are subclasses of ModelInvariantIndex) and retrieves invariant
96  * storage indexes for them. It also builds threading caches and
97  * attempts to do some "easy" threading. If it succeeds in threading
98  * and some conditions apply then it also attaches the items to the view.
99  * Any unattached message is placed in a list.
100  *
101  * Pass 2 scans the list of messages that haven't been attached in
102  * the first pass and performs perfect and reference based threading.
103  * Since grouping of messages may depend on the "shape" of the thread
104  * then certain threads aren't attached to the view yet.
105  * Unassigned messages get stuffed into a list waiting for Pass3
106  * or directly to a list waiting for Pass4 (that is, Pass3 may be skipped
107  * if there is no hope to find an imperfect parent by subject based threading).
108  *
109  * Pass 3 scans the list of messages that haven't been attached in
110  * the first and second passes and performs subject based threading.
111  * Since grouping of messages may depend on the "shape" of the thread
112  * then certain threads aren't attached to the view yet.
113  * Anything unattached gets stuffed into the list waiting for Pass4.
114  *
115  * Pass 4 scans the unattached threads and puts them in the appropriate
116  * groups. After this pass nothing is unattached.
117  *
118  * Pass 5 eventually re-sorts the groups and removes the empty ones.
119  *
120  * For a "View Cleanup" we still have 5 passes.
121  *
122  * Pass 1 scans the list of invalidated ModelInvariantIndex-es, casts
123  * them to MessageItem objects and detaches them from the view.
124  * The orphan children of the destroyed items get stuffed in the list
125  * of unassigned messages that has been used also in the "View Fill" task above.
126  *
127  * Pass 2, 3, 4 and 5: same as "View Fill", just operating on the "orphaned"
128  * messages that need to be reattached to the view.
129  *
130  * For a "View Update" we still have 5 passes.
131  *
132  * Pass 1 scans the list of ModelInvariantIndex-es that need an update, casts
133  * them to MessageItem objects and handles the updates from storage.
134  * The updates may cause a regrouping so items might be stuffed in one
135  * of the lists for pass 4 or 5.
136  *
137  * Pass 2, 3 and 4 are simply empty.
138  *
139  * Pass 5: same as "View Fill", just operating on groups that require updates
140  * after the messages have been moved in pass 1.
141  *
142  * That's why we in fact have Pass1Fill, Pass1Cleanup, Pass1Update, Pass2, Pass3, Pass4 and Pass5 below.
143  * Pass1Fill, Pass1Cleanup and Pass1Update are exclusive and all of them proceed with Pass2 when finished.
144  */
145 class ViewItemJob
146 {
147 public:
148     enum Pass {
149         Pass1Fill = 0, ///< Build threading caches, *TRY* to do some threading, try to attach something to the view
150         Pass1Cleanup = 1, ///< Kill messages, build list of orphans
151         Pass1Update = 2, ///< Update messages
152         Pass2 = 3, ///< Thread everything by using caches, try to attach more to the view
153         Pass3 = 4, ///< Do more threading (this time try to guess), try to attach more to the view
154         Pass4 = 5, ///< Attach anything is still unattached
155         Pass5 = 6, ///< Eventually Re-sort group headers and remove the empty ones
156         LastIndex = 7 ///< Keep this at the end, needed to get the size of the enum
157     };
158 
159 private:
160     // Data for "View Fill" jobs
161     int mStartIndex; ///< The first index (in the underlying storage) of this job
162     int mCurrentIndex; ///< The current index (in the underlying storage) of this job
163     int mEndIndex; ///< The last index (in the underlying storage) of this job
164 
165     // Data for "View Cleanup" jobs
166     QList<ModelInvariantIndex *> *mInvariantIndexList; ///< Owned list of shallow pointers
167 
168     // Common data
169 
170     // The maximum time that we can spend "at once" inside viewItemJobStep() (milliseconds)
171     // The bigger this value, the larger chunks of work we do at once and less the time
172     // we loose in "breaking and resuming" the job. On the other side large values tend
173     // to make the view less responsive up to a "freeze" perception if this value is larger
174     // than 2000.
175     int mChunkTimeout;
176 
177     // The interval between two fillView steps. The larger the interval, the more interactivity
178     // we have. The shorter the interval the more work we get done per second.
179     int mIdleInterval;
180 
181     // The minimum number of messages we process in every viewItemJobStep() call
182     // The larger this value the less time we loose in checking the timeout every N messages.
183     // On the other side, making this very large may make the view less responsive
184     // if we're processing very few messages at a time and very high values (say > 10000) may
185     // eventually make our job unbreakable until the end.
186     int mMessageCheckCount;
187     Pass mCurrentPass;
188 
189     // If this parameter is true then this job uses a "disconnected" UI.
190     // It's FAR faster since we don't need to call beginInsertRows()/endInsertRows()
191     // and we simply Q_EMIT a layoutChanged() at the end. It can be done only as the first
192     // job though: subsequent jobs can't use layoutChanged() as it looses the expanded
193     // state of items.
194     bool mDisconnectUI;
195 
196 public:
197     /**
198      * Creates a "View Fill" operation job
199      */
ViewItemJob(int startIndex,int endIndex,int chunkTimeout,int idleInterval,int messageCheckCount,bool disconnectUI=false)200     ViewItemJob(int startIndex, int endIndex, int chunkTimeout, int idleInterval, int messageCheckCount, bool disconnectUI = false)
201         : mStartIndex(startIndex)
202         , mCurrentIndex(startIndex)
203         , mEndIndex(endIndex)
204         , mInvariantIndexList(nullptr)
205         , mChunkTimeout(chunkTimeout)
206         , mIdleInterval(idleInterval)
207         , mMessageCheckCount(messageCheckCount)
208         , mCurrentPass(Pass1Fill)
209         , mDisconnectUI(disconnectUI)
210     {
211     }
212 
213     /**
214      * Creates a "View Cleanup" or "View Update" operation job
215      */
ViewItemJob(Pass pass,QList<ModelInvariantIndex * > * invariantIndexList,int chunkTimeout,int idleInterval,int messageCheckCount)216     ViewItemJob(Pass pass, QList<ModelInvariantIndex *> *invariantIndexList, int chunkTimeout, int idleInterval, int messageCheckCount)
217         : mStartIndex(0)
218         , mCurrentIndex(0)
219         , mEndIndex(invariantIndexList->count() - 1)
220         , mInvariantIndexList(invariantIndexList)
221         , mChunkTimeout(chunkTimeout)
222         , mIdleInterval(idleInterval)
223         , mMessageCheckCount(messageCheckCount)
224         , mCurrentPass(pass)
225         , mDisconnectUI(false)
226     {
227     }
228 
~ViewItemJob()229     ~ViewItemJob()
230     {
231         delete mInvariantIndexList;
232     }
233 
234 public:
startIndex() const235     int startIndex() const
236     {
237         return mStartIndex;
238     }
239 
setStartIndex(int startIndex)240     void setStartIndex(int startIndex)
241     {
242         mStartIndex = startIndex;
243         mCurrentIndex = startIndex;
244     }
245 
currentIndex() const246     int currentIndex() const
247     {
248         return mCurrentIndex;
249     }
250 
setCurrentIndex(int currentIndex)251     void setCurrentIndex(int currentIndex)
252     {
253         mCurrentIndex = currentIndex;
254     }
255 
endIndex() const256     int endIndex() const
257     {
258         return mEndIndex;
259     }
260 
setEndIndex(int endIndex)261     void setEndIndex(int endIndex)
262     {
263         mEndIndex = endIndex;
264     }
265 
currentPass() const266     Pass currentPass() const
267     {
268         return mCurrentPass;
269     }
270 
setCurrentPass(Pass pass)271     void setCurrentPass(Pass pass)
272     {
273         mCurrentPass = pass;
274     }
275 
idleInterval() const276     int idleInterval() const
277     {
278         return mIdleInterval;
279     }
280 
chunkTimeout() const281     int chunkTimeout() const
282     {
283         return mChunkTimeout;
284     }
285 
messageCheckCount() const286     int messageCheckCount() const
287     {
288         return mMessageCheckCount;
289     }
290 
invariantIndexList() const291     QList<ModelInvariantIndex *> *invariantIndexList() const
292     {
293         return mInvariantIndexList;
294     }
295 
disconnectUI() const296     bool disconnectUI() const
297     {
298         return mDisconnectUI;
299     }
300 };
301 } // namespace Core
302 } // namespace MessageList
303 
304 using namespace MessageList::Core;
305 
Model(View * pParent)306 Model::Model(View *pParent)
307     : QAbstractItemModel(pParent)
308     , d(new ModelPrivate(this))
309 {
310     d->mRecursionCounterForReset = 0;
311     d->mStorageModel = nullptr;
312     d->mView = pParent;
313     d->mAggregation = nullptr;
314     d->mTheme = nullptr;
315     d->mSortOrder = nullptr;
316     d->mFilter = nullptr;
317     d->mPersistentSetManager = nullptr;
318     d->mInLengthyJobBatch = false;
319     d->mLastSelectedMessageInFolder = nullptr;
320     d->mLoading = false;
321 
322     d->mRootItem = new Item(Item::InvisibleRoot);
323     d->mRootItem->setViewable(nullptr, true);
324 
325     d->mFillStepTimer.setSingleShot(true);
326     d->mInvariantRowMapper = new ModelInvariantRowMapper();
327     d->mModelForItemFunctions = this;
328     connect(&d->mFillStepTimer, &QTimer::timeout, this, [this]() {
329         d->viewItemJobStep();
330     });
331 
332     d->mCachedTodayLabel = i18n("Today");
333     d->mCachedYesterdayLabel = i18n("Yesterday");
334     d->mCachedUnknownLabel = i18nc("Unknown date", "Unknown");
335     d->mCachedLastWeekLabel = i18n("Last Week");
336     d->mCachedTwoWeeksAgoLabel = i18n("Two Weeks Ago");
337     d->mCachedThreeWeeksAgoLabel = i18n("Three Weeks Ago");
338     d->mCachedFourWeeksAgoLabel = i18n("Four Weeks Ago");
339     d->mCachedFiveWeeksAgoLabel = i18n("Five Weeks Ago");
340 
341     d->mCachedWatchedOrIgnoredStatusBits = Akonadi::MessageStatus::statusIgnored().toQInt32() | Akonadi::MessageStatus::statusWatched().toQInt32();
342 
343     connect(_k_heartBeatTimer(), &QTimer::timeout, this, [this]() {
344         d->checkIfDateChanged();
345     });
346 
347     if (!_k_heartBeatTimer->isActive()) { // First model starts it
348         _k_heartBeatTimer->start(1min); // 1 minute
349     }
350 }
351 
~Model()352 Model::~Model()
353 {
354     setStorageModel(nullptr);
355 
356     d->clearJobList();
357     d->mOldestItem = nullptr;
358     d->mNewestItem = nullptr;
359     d->clearUnassignedMessageLists();
360     d->clearOrphanChildrenHash();
361     d->clearThreadingCacheReferencesIdMD5ToMessageItem();
362     d->clearThreadingCacheMessageSubjectMD5ToMessageItem();
363     delete d->mPersistentSetManager;
364     // Delete the invariant row mapper before removing the items.
365     // It's faster since the items will not need to call the invariant
366     delete d->mInvariantRowMapper;
367     delete d->mRootItem;
368 
369 }
370 
setAggregation(const Aggregation * aggregation)371 void Model::setAggregation(const Aggregation *aggregation)
372 {
373     d->mAggregation = aggregation;
374     d->mView->setRootIsDecorated((d->mAggregation->grouping() == Aggregation::NoGrouping) && (d->mAggregation->threading() != Aggregation::NoThreading));
375 }
376 
setTheme(const Theme * theme)377 void Model::setTheme(const Theme *theme)
378 {
379     d->mTheme = theme;
380 }
381 
setSortOrder(const SortOrder * sortOrder)382 void Model::setSortOrder(const SortOrder *sortOrder)
383 {
384     d->mSortOrder = sortOrder;
385 }
386 
sortOrder() const387 const SortOrder *Model::sortOrder() const
388 {
389     return d->mSortOrder;
390 }
391 
setFilter(const Filter * filter)392 void Model::setFilter(const Filter *filter)
393 {
394     d->mFilter = filter;
395 
396     if (d->mFilter) {
397         connect(d->mFilter, &Filter::finished, this, [this]() {
398             d->slotApplyFilter();
399         });
400     }
401 
402     d->slotApplyFilter();
403 }
404 
slotApplyFilter()405 void ModelPrivate::slotApplyFilter()
406 {
407     auto childList = mRootItem->childItems();
408     if (!childList) {
409         return;
410     }
411 
412     QModelIndex idx; // invalid
413 
414     QApplication::setOverrideCursor(Qt::WaitCursor);
415     for (const auto child : std::as_const(*childList)) {
416         applyFilterToSubtree(child, idx);
417     }
418 
419     QApplication::restoreOverrideCursor();
420 }
421 
applyFilterToSubtree(Item * item,const QModelIndex & parentIndex)422 bool ModelPrivate::applyFilterToSubtree(Item *item, const QModelIndex &parentIndex)
423 {
424     // This function applies the current filter (eventually empty)
425     // to a message tree starting at "item".
426 
427     if (!mModelForItemFunctions) {
428         qCWarning(MESSAGELIST_LOG) << "Cannot apply filter, the UI must be not disconnected.";
429         return true;
430     }
431     Q_ASSERT(item); // the item must obviously be valid
432     Q_ASSERT(item->isViewable()); // the item must be viewable
433 
434     // Apply to children first
435 
436     auto childList = item->childItems();
437 
438     bool childrenMatch = false;
439 
440     QModelIndex thisIndex = q->index(item, 0);
441 
442     if (childList) {
443         for (const auto child : std::as_const(*childList)) {
444             if (applyFilterToSubtree(child, thisIndex)) {
445                 childrenMatch = true;
446             }
447         }
448     }
449 
450     if (!mFilter) { // empty filter always matches (but does not expand items)
451         mView->setRowHidden(thisIndex.row(), parentIndex, false);
452         return true;
453     }
454 
455     if (childrenMatch) {
456         mView->setRowHidden(thisIndex.row(), parentIndex, false);
457 
458         if (!mView->isExpanded(thisIndex)) {
459             mView->expand(thisIndex);
460         }
461         return true;
462     }
463 
464     if (item->type() == Item::Message) {
465         if (mFilter->match((MessageItem *)item)) {
466             mView->setRowHidden(thisIndex.row(), parentIndex, false);
467             return true;
468         }
469     } // else this is a group header and it never explicitly matches
470 
471     // filter doesn't match, hide the item
472     mView->setRowHidden(thisIndex.row(), parentIndex, true);
473 
474     return false;
475 }
476 
columnCount(const QModelIndex & parent) const477 int Model::columnCount(const QModelIndex &parent) const
478 {
479     if (!d->mTheme) {
480         return 0;
481     }
482     if (parent.column() > 0) {
483         return 0;
484     }
485     return d->mTheme->columns().count();
486 }
487 
data(const QModelIndex & index,int role) const488 QVariant Model::data(const QModelIndex &index, int role) const
489 {
490     /// this is called only when Akonadi is using the selectionmodel
491     ///  for item actions. since akonadi uses the ETM ItemRoles, and the
492     ///  messagelist uses its own internal roles, here we respond
493     ///  to the ETM ones.
494 
495     auto item = static_cast<Item *>(index.internalPointer());
496 
497     switch (role) {
498     /// taken from entitytreemodel.h
499     case Qt::UserRole + 1: // EntityTreeModel::ItemIdRole
500         if (item->type() == MessageList::Core::Item::Message) {
501             auto mItem = static_cast<MessageItem *>(item);
502             return QVariant::fromValue(mItem->akonadiItem().id());
503         } else {
504             return QVariant();
505         }
506         break;
507     case Qt::UserRole + 2: // EntityTreeModel::ItemRole
508         if (item->type() == MessageList::Core::Item::Message) {
509             auto mItem = static_cast<MessageItem *>(item);
510             return QVariant::fromValue(mItem->akonadiItem());
511         } else {
512             return QVariant();
513         }
514         break;
515     case Qt::UserRole + 3: // EntityTreeModel::MimeTypeRole
516         if (item->type() == MessageList::Core::Item::Message) {
517             return QStringLiteral("message/rfc822");
518         } else {
519             return QVariant();
520         }
521         break;
522     case Qt::AccessibleTextRole:
523         if (item->type() == MessageList::Core::Item::Message) {
524             auto mItem = static_cast<MessageItem *>(item);
525             return mItem->accessibleText(d->mTheme, index.column());
526         } else if (item->type() == MessageList::Core::Item::GroupHeader) {
527             if (index.column() > 0) {
528                 return QString();
529             }
530             auto hItem = static_cast<GroupHeaderItem *>(item);
531             return hItem->label();
532         }
533         return QString();
534         break;
535     default:
536         return QVariant();
537     }
538 }
539 
headerData(int section,Qt::Orientation,int role) const540 QVariant Model::headerData(int section, Qt::Orientation, int role) const
541 {
542     if (!d->mTheme) {
543         return QVariant();
544     }
545 
546     auto column = d->mTheme->column(section);
547     if (!column) {
548         return QVariant();
549     }
550 
551     if (d->mStorageModel && column->isSenderOrReceiver() && (role == Qt::DisplayRole)) {
552         if (d->mStorageModelContainsOutboundMessages) {
553             return QVariant(i18n("Receiver"));
554         }
555         return QVariant(i18n("Sender"));
556     }
557 
558     const bool columnPixmapEmpty(column->pixmapName().isEmpty());
559     if ((role == Qt::DisplayRole) && columnPixmapEmpty) {
560         return QVariant(column->label());
561     } else if ((role == Qt::ToolTipRole) && !columnPixmapEmpty) {
562         return QVariant(column->label());
563     } else if ((role == Qt::DecorationRole) && !columnPixmapEmpty) {
564         return QVariant(QIcon::fromTheme(column->pixmapName()));
565     }
566 
567     return QVariant();
568 }
569 
index(Item * item,int column) const570 QModelIndex Model::index(Item *item, int column) const
571 {
572     if (!d->mModelForItemFunctions) {
573         return QModelIndex(); // called with disconnected UI: the item isn't known on the Qt side, yet
574     }
575 
576     if (!item) {
577         return QModelIndex();
578     }
579     // FIXME: This function is a bottleneck (the caching in indexOfChildItem only works 30% of the time)
580     auto par = item->parent();
581     if (!par) {
582         if (item != d->mRootItem) {
583             item->dump(QString());
584         }
585         return QModelIndex();
586     }
587 
588     const int index = par->indexOfChildItem(item);
589     if (index < 0) {
590         return QModelIndex(); // BUG
591     }
592     return createIndex(index, column, item);
593 }
594 
index(int row,int column,const QModelIndex & parent) const595 QModelIndex Model::index(int row, int column, const QModelIndex &parent) const
596 {
597     if (!d->mModelForItemFunctions) {
598         return QModelIndex(); // called with disconnected UI: the item isn't known on the Qt side, yet
599     }
600 
601 #ifdef READD_THIS_IF_YOU_WANT_TO_PASS_MODEL_TEST
602     if (column < 0) {
603         return QModelIndex(); // senseless column (we could optimize by skipping this check but ModelTest from trolltech is pedantic)
604     }
605 #endif
606 
607     const Item *item;
608     if (parent.isValid()) {
609         item = static_cast<const Item *>(parent.internalPointer());
610         if (!item) {
611             return QModelIndex(); // should never happen
612         }
613     } else {
614         item = d->mRootItem;
615     }
616 
617     if (parent.column() > 0) {
618         return QModelIndex(); // parent column is not 0: shouldn't have children (as per Qt documentation)
619     }
620 
621     Item *child = item->childItem(row);
622     if (!child) {
623         return QModelIndex(); // no such row in parent
624     }
625     return createIndex(row, column, child);
626 }
627 
parent(const QModelIndex & modelIndex) const628 QModelIndex Model::parent(const QModelIndex &modelIndex) const
629 {
630     Q_ASSERT(d->mModelForItemFunctions); // should be never called with disconnected UI
631 
632     if (!modelIndex.isValid()) {
633         return QModelIndex(); // should never happen
634     }
635     auto item = static_cast<Item *>(modelIndex.internalPointer());
636     if (!item) {
637         return QModelIndex();
638     }
639     auto par = item->parent();
640     if (!par) {
641         return QModelIndex(); // should never happen
642     }
643     // return index( par, modelIndex.column() );
644     return index(par, 0); // parents are always in column 0 (as per Qt documentation)
645 }
646 
rowCount(const QModelIndex & parent) const647 int Model::rowCount(const QModelIndex &parent) const
648 {
649     if (!d->mModelForItemFunctions) {
650         return 0; // called with disconnected UI
651     }
652 
653     const Item *item;
654     if (parent.isValid()) {
655         item = static_cast<const Item *>(parent.internalPointer());
656         if (!item) {
657             return 0; // should never happen
658         }
659     } else {
660         item = d->mRootItem;
661     }
662 
663     if (!item->isViewable()) {
664         return 0;
665     }
666 
667     return item->childItemCount();
668 }
669 
670 class RecursionPreventer
671 {
672 public:
RecursionPreventer(int & counter)673     RecursionPreventer(int &counter)
674         : mCounter(counter)
675     {
676         mCounter++;
677     }
678 
~RecursionPreventer()679     ~RecursionPreventer()
680     {
681         mCounter--;
682     }
683 
isRecursive() const684     bool isRecursive() const
685     {
686         return mCounter > 1;
687     }
688 
689 private:
690     int &mCounter;
691 };
692 
storageModel() const693 StorageModel *Model::storageModel() const
694 {
695     return d->mStorageModel;
696 }
697 
clear()698 void ModelPrivate::clear()
699 {
700     q->beginResetModel();
701     if (mFillStepTimer.isActive()) {
702         mFillStepTimer.stop();
703     }
704 
705     // Kill pre-selection at this stage
706     mPreSelectionMode = PreSelectNone;
707     mLastSelectedMessageInFolder = nullptr;
708     mOldestItem = nullptr;
709     mNewestItem = nullptr;
710 
711     // Reset the row mapper before removing items
712     // This is faster since the items don't need to access the mapper.
713     mInvariantRowMapper->modelReset();
714 
715     clearJobList();
716     clearUnassignedMessageLists();
717     clearOrphanChildrenHash();
718     mGroupHeaderItemHash.clear();
719     mGroupHeadersThatNeedUpdate.clear();
720     mThreadingCacheMessageIdMD5ToMessageItem.clear();
721     mThreadingCacheMessageInReplyToIdMD5ToMessageItem.clear();
722     clearThreadingCacheReferencesIdMD5ToMessageItem();
723     clearThreadingCacheMessageSubjectMD5ToMessageItem();
724     mViewItemJobStepChunkTimeout = 100;
725     mViewItemJobStepIdleInterval = 10;
726     mViewItemJobStepMessageCheckCount = 10;
727     delete mPersistentSetManager;
728     mPersistentSetManager = nullptr;
729     mCurrentItemToRestoreAfterViewItemJobStep = nullptr;
730 
731     mTodayDate = QDate::currentDate();
732 
733     // FIXME: CLEAR THE FILTER HERE AS WE CAN'T APPLY IT WITH UI DISCONNECTED!
734 
735     mRootItem->killAllChildItems();
736 
737     q->endResetModel();
738     // Q_EMIT headerDataChanged();
739 
740     mView->selectionModel()->clearSelection();
741 }
742 
setStorageModel(StorageModel * storageModel,PreSelectionMode preSelectionMode)743 void Model::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode)
744 {
745     // Prevent a case of recursion when opening a folder that has a message and the folder was
746     // never opened before.
747     RecursionPreventer preventer(d->mRecursionCounterForReset);
748     if (preventer.isRecursive()) {
749         return;
750     }
751 
752     d->clear();
753 
754     if (d->mStorageModel) {
755         // Disconnect all signals from old storageModel
756         std::for_each(d->mStorageModelConnections.cbegin(), d->mStorageModelConnections.cend(), [](const QMetaObject::Connection &c) -> bool {
757             return QObject::disconnect(c);
758         });
759         d->mStorageModelConnections.clear();
760     }
761 
762     const bool isReload = (d->mStorageModel == storageModel);
763     d->mStorageModel = storageModel;
764 
765     if (!d->mStorageModel) {
766         return; // no folder: nothing to fill
767     }
768 
769     // Save threading cache of the previous folder, but only if the cache was
770     // enabled and a different folder is being loaded - reload of the same folder
771     // means change in aggregation in which case we will have to re-build the
772     // cache so there's no point saving the current threading cache.
773     if (d->mThreadingCache.isEnabled() && !isReload) {
774         d->mThreadingCache.save();
775     } else {
776         if (isReload) {
777             qCDebug(MESSAGELIST_LOG) << "Identical folder reloaded, not saving old threading cache";
778         } else {
779             qCDebug(MESSAGELIST_LOG) << "Threading disabled in previous folder, not saving threading cache";
780         }
781     }
782     // Load threading cache for the new folder, but only if threading is enabled,
783     // otherwise we would just be caching a flat list.
784     if (d->mAggregation->threading() != Aggregation::NoThreading) {
785         d->mThreadingCache.setEnabled(true);
786         d->mThreadingCache.load(d->mStorageModel->id(), d->mAggregation);
787     } else {
788         // No threading, no cache - don't even bother inserting entries into the
789         // cache or trying to look them up there
790         d->mThreadingCache.setEnabled(false);
791         qCDebug(MESSAGELIST_LOG) << "Threading disabled in folder" << d->mStorageModel->id() << ", not using threading cache";
792     }
793 
794     d->mPreSelectionMode = preSelectionMode;
795     d->mStorageModelContainsOutboundMessages = d->mStorageModel->containsOutboundMessages();
796 
797     d->mStorageModelConnections = {connect(d->mStorageModel,
798                                            &StorageModel::rowsInserted,
799                                            this,
800                                            [this](const QModelIndex &parent, int first, int last) {
801                                                d->slotStorageModelRowsInserted(parent, first, last);
802                                            }),
803                                    connect(d->mStorageModel,
804                                            &StorageModel::rowsRemoved,
805                                            this,
806                                            [this](const QModelIndex &parent, int first, int last) {
807                                                d->slotStorageModelRowsRemoved(parent, first, last);
808                                            }),
809                                    connect(d->mStorageModel,
810                                            &StorageModel::layoutChanged,
811                                            this,
812                                            [this]() {
813                                                d->slotStorageModelLayoutChanged();
814                                            }),
815                                    connect(d->mStorageModel,
816                                            &StorageModel::modelReset,
817                                            this,
818                                            [this]() {
819                                                d->slotStorageModelLayoutChanged();
820                                            }),
821                                    connect(d->mStorageModel,
822                                            &StorageModel::dataChanged,
823                                            this,
824                                            [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
825                                                d->slotStorageModelDataChanged(topLeft, bottomRight);
826                                            }),
827                                    connect(d->mStorageModel, &StorageModel::headerDataChanged, this, [this](Qt::Orientation orientation, int first, int last) {
828                                        d->slotStorageModelHeaderDataChanged(orientation, first, last);
829                                    })};
830 
831     if (d->mStorageModel->rowCount() == 0) {
832         return; // folder empty: nothing to fill
833     }
834 
835     // Here we use different strategies based on user preference and the folder size.
836     // The knobs we can tune are:
837     //
838     // - The number of jobs used to scan the whole folder and their order
839     //
840     //   There are basically two approaches to this. One is the "single big job"
841     //   approach. It scans the folder from the beginning to the end in a single job
842     //   entry. The job passes are done only once. It's advantage is that it's simpler
843     //   and it's less likely to generate imperfect parent threadings. The bad
844     //   side is that since the folders are "sort of" date ordered then the most interesting
845     //   messages show up at the end of the work. Not nice for large folders.
846     //   The other approach uses two jobs. This is a bit slower but smarter strategy.
847     //   First we scan the latest 1000 messages and *then* take care of the older ones.
848     //   This will show up the most interesting messages almost immediately. (Well...
849     //   All this assuming that the underlying storage always appends the newly arrived messages)
850     //   The strategy is slower since it  generates some imperfect parent threadings which must be
851     //   adjusted by the second job. For instance, in my kernel mailing list folder this "smart" approach
852     //   generates about 150 additional imperfectly threaded children... but the "today"
853     //   messages show up almost immediately. The two-chunk job also makes computing
854     //   the percentage user feedback a little harder and might break some optimization
855     //   in the insertions (we're able to optimize appends and prepends but a chunked
856     //   job is likely to split our work at a boundary where messages are always inserted
857     //   in the middle of the list).
858     //
859     // - The maximum time to spend inside a single job step
860     //
861     //   The larger this time, the greater the number of messages per second that this
862     //   engine can process but also greater time with frozen UI -> less interactivity.
863     //   Reasonable values start at 50 msecs. Values larger than 300 msecs are very likely
864     //   to be perceived by the user as UI non-reactivity.
865     //
866     // - The number of messages processed in each job step subchunk.
867     //
868     //   A job subchunk is processed without checking the maximum time above. This means
869     //   that each job step will process at least the number of messages specified by this value.
870     //   Very low values mean that we respect the maximum time very carefully but we also
871     //   waste time to check if we ran out of time :)
872     //   Very high values are likely to cause the engine to not respect the maximum step time.
873     //   Reasonable values go from 5 to 100.
874     //
875     // - The "idle" time between two steps
876     //
877     //   The lower this time, the greater the number of messages per second that this
878     //   engine can process but also lower time for the UI to process events -> less interactivity.
879     //   A value of 0 here means that Qt will trigger the timer as soon as it has some
880     //   idle time to spend. UI events will be still processed but slowdowns are possible.
881     //   0 is reasonable though. Values larger than 200 will tend to make the total job
882     //   completion times high.
883     //
884 
885     // If we have no filter it seems that we can apply a huge optimization.
886     // We disconnect the UI for the first huge filling job. This allows us
887     // to save the extremely expensive beginInsertRows()/endInsertRows() calls
888     // and call a single layoutChanged() at the end. This slows down a lot item
889     // expansion. But on the other side if only few items need to be expanded
890     // then this strategy is better. If filtering is enabled then this strategy
891     // isn't applicable (because filtering requires interaction with the UI
892     // while the data is loading).
893 
894     // So...
895 
896     // For the very first small chunk it's ok to work with disconnected UI as long
897     // as we have no filter. The first small chunk is always 1000 messages, so
898     // even if all of them are expanded, it's still somewhat acceptable.
899     bool canDoFirstSmallChunkWithDisconnectedUI = !d->mFilter;
900 
901     // Larger works need a bigger condition: few messages must be expanded in the end.
902     bool canDoJobWithDisconnectedUI = // we have no filter
903         !d->mFilter
904         && (
905             // we do no threading at all
906             (d->mAggregation->threading() == Aggregation::NoThreading) || // or we never expand threads
907             (d->mAggregation->threadExpandPolicy() == Aggregation::NeverExpandThreads) || // or we expand threads but we'll be going to expand really only a few
908             (
909                 // so we don't expand them all
910                 (d->mAggregation->threadExpandPolicy() != Aggregation::AlwaysExpandThreads) && // and we'd expand only a few in fact
911                 (d->mStorageModel->initialUnreadRowCountGuess() < 1000)));
912 
913     switch (d->mAggregation->fillViewStrategy()) {
914     case Aggregation::FavorInteractivity:
915         // favor interactivity
916         if ((!canDoJobWithDisconnectedUI) && (d->mStorageModel->rowCount() > 3000)) { // empiric value
917             // First a small job with the most recent messages. Large chunk, small (but non zero) idle interval
918             // and a larger number of messages to process at once.
919             auto job1 =
920                 new ViewItemJob(d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 200, 20, 100, canDoFirstSmallChunkWithDisconnectedUI);
921             d->mViewItemJobs.append(job1);
922             // Then a larger job with older messages. Small chunk, bigger idle interval, small number of messages to
923             // process at once.
924             auto job2 = new ViewItemJob(0, d->mStorageModel->rowCount() - 1001, 100, 50, 10, false);
925             d->mViewItemJobs.append(job2);
926 
927             // We could even extremize this by splitting the folder in several
928             // chunks and scanning them from the newest to the oldest... but the overhead
929             // due to imperfectly threaded children would be probably too big.
930         } else {
931             // small folder or can be done with disconnected UI: single chunk work.
932             // Lag the CPU a bit more but not too much to destroy even the earliest interactivity.
933             auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 150, 30, 30, canDoJobWithDisconnectedUI);
934             d->mViewItemJobs.append(job);
935         }
936         break;
937     case Aggregation::FavorSpeed:
938         // More batchy jobs, still interactive to a certain degree
939         if ((!canDoJobWithDisconnectedUI) && (d->mStorageModel->rowCount() > 3000)) { // empiric value
940             // large folder, but favor speed
941             auto job1 =
942                 new ViewItemJob(d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoFirstSmallChunkWithDisconnectedUI);
943             d->mViewItemJobs.append(job1);
944             auto job2 = new ViewItemJob(0, d->mStorageModel->rowCount() - 1001, 200, 0, 10, false);
945             d->mViewItemJobs.append(job2);
946         } else {
947             // small folder or can be done with disconnected UI and favor speed: single chunk work.
948             // Lag the CPU more, get more work done
949             auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoJobWithDisconnectedUI);
950             d->mViewItemJobs.append(job);
951         }
952         break;
953     case Aggregation::BatchNoInteractivity: {
954         // one large job, never interrupt, block UI
955         auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 60000, 0, 100000, canDoJobWithDisconnectedUI);
956         d->mViewItemJobs.append(job);
957         break;
958     }
959     default:
960         qCWarning(MESSAGELIST_LOG) << "Unrecognized fill view strategy";
961         Q_ASSERT(false);
962         break;
963     }
964 
965     d->mLoading = true;
966 
967     d->viewItemJobStep();
968 }
969 
checkIfDateChanged()970 void ModelPrivate::checkIfDateChanged()
971 {
972     // This function is called by MessageList::Core::Manager once in a while (every 1 minute or sth).
973     // It is used to check if the current date has changed (with respect to mTodayDate).
974     //
975     // Our message items cache the formatted dates (as formatting them
976     // on the fly would be too expensive). We also cache the labels of the groups which often display dates.
977     // When the date changes we would need to fix all these strings.
978     //
979     // A dedicated algorithm to refresh the labels of the items would be either too complex
980     // or would block on large trees. Fixing the labels of the groups is also quite hard...
981     //
982     // So to keep the things simple we just reload the view.
983 
984     if (!mStorageModel) {
985         return; // nothing to do
986     }
987 
988     if (mLoading) {
989         return; // not now
990     }
991 
992     if (!mViewItemJobs.isEmpty()) {
993         return; // not now
994     }
995 
996     if (mTodayDate == QDate::currentDate()) {
997         return; // date not changed
998     }
999 
1000     // date changed, reload the view (and try to preserve the current selection)
1001     q->setStorageModel(mStorageModel, PreSelectLastSelected);
1002 }
1003 
setPreSelectionMode(PreSelectionMode preSelect)1004 void Model::setPreSelectionMode(PreSelectionMode preSelect)
1005 {
1006     d->mPreSelectionMode = preSelect;
1007     d->mLastSelectedMessageInFolder = nullptr;
1008 }
1009 
1010 //
1011 // The "view fill" algorithm implemented in the functions below is quite smart but also quite complex.
1012 // It's governed by the following goals:
1013 //
1014 // - Be flexible: allow different configurations from "unsorted flat list" to a "grouped and threaded
1015 //     list with different sorting algorithms applied to each aggregation level"
1016 // - Be reasonably fast
1017 // - Be non blocking: UI shouldn't freeze while the algorithm is running
1018 // - Be interruptible: user must be able to abort the execution and just switch to another folder in the middle
1019 //
1020 
clearUnassignedMessageLists()1021 void ModelPrivate::clearUnassignedMessageLists()
1022 {
1023     // This is a bit tricky...
1024     // The three unassigned message lists contain messages that have been created
1025     // but not yet attached to the view. There may be two major cases for a message:
1026     // - it has no parent -> it must be deleted and it will delete its children too
1027     // - it has a parent -> it must NOT be deleted since it will be deleted by its parent.
1028 
1029     // Sometimes the things get a little complicated since in Pass2 and Pass3
1030     // we have transitional states in that the MessageItem object can be in two of these lists.
1031 
1032     // WARNING: This function does NOT fixup mNewestItem and mOldestItem. If one of these
1033     // two messages is in the lists below, it's deleted and the member becomes a dangling pointer.
1034     // The caller must ensure that both mNewestItem and mOldestItem are set to 0
1035     // and this is enforced in the assert below to avoid errors. This basically means
1036     // that this function should be called only when the storage model changes or
1037     // when the model is destroyed.
1038     Q_ASSERT((mOldestItem == nullptr) && (mNewestItem == nullptr));
1039 
1040     if (!mUnassignedMessageListForPass2.isEmpty()) {
1041         // We're actually in Pass1* or Pass2: everything is mUnassignedMessageListForPass2
1042         // Something may *also* be in mUnassignedMessageListForPass3 and mUnassignedMessageListForPass4
1043         // but that are duplicates for sure.
1044 
1045         // We can't just sweep the list and delete parentless items since each delete
1046         // could kill children which are somewhere AFTER in the list: accessing the children
1047         // would then lead to a SIGSEGV. We first sweep the list gathering parentless
1048         // items and *then* delete them without accessing the parented ones.
1049 
1050         QList<MessageItem *> parentless;
1051         for (const auto mi : std::as_const(mUnassignedMessageListForPass2)) {
1052             if (!mi->parent()) {
1053                 parentless.append(mi);
1054             }
1055         }
1056 
1057         for (const auto mi : std::as_const(parentless)) {
1058             delete mi;
1059         }
1060 
1061         mUnassignedMessageListForPass2.clear();
1062         // Any message these list contain was also in mUnassignedMessageListForPass2
1063         mUnassignedMessageListForPass3.clear();
1064         mUnassignedMessageListForPass4.clear();
1065         return;
1066     }
1067 
1068     // mUnassignedMessageListForPass2 is empty
1069 
1070     if (!mUnassignedMessageListForPass3.isEmpty()) {
1071         // We're actually at the very end of Pass2 or inside Pass3
1072         // Pass2 pushes stuff in mUnassignedMessageListForPass3 *or* mUnassignedMessageListForPass4
1073         // Pass3 pushes stuff from mUnassignedMessageListForPass3 to mUnassignedMessageListForPass4
1074         // So if we're in Pass2 then the two lists contain distinct messages but if we're in Pass3
1075         // then the two lists may contain the same messages.
1076 
1077         if (!mUnassignedMessageListForPass4.isEmpty()) {
1078             // We're actually in Pass3: the messiest one.
1079 
1080             QSet<MessageItem *> itemsToDelete;
1081             for (const auto mi : std::as_const(mUnassignedMessageListForPass3)) {
1082                 if (!mi->parent()) {
1083                     itemsToDelete.insert(mi);
1084                 }
1085             }
1086             for (const auto mi : std::as_const(mUnassignedMessageListForPass4)) {
1087                 if (!mi->parent()) {
1088                     itemsToDelete.insert(mi);
1089                 }
1090             }
1091             for (const auto mi : std::as_const(itemsToDelete)) {
1092                 delete mi;
1093             }
1094 
1095             mUnassignedMessageListForPass3.clear();
1096             mUnassignedMessageListForPass4.clear();
1097             return;
1098         }
1099 
1100         // mUnassignedMessageListForPass4 is empty so we must be at the end of a very special kind of Pass2
1101         // We have the same problem as in mUnassignedMessageListForPass2.
1102         QList<MessageItem *> parentless;
1103         for (const auto mi : std::as_const(mUnassignedMessageListForPass3)) {
1104             if (!mi->parent()) {
1105                 parentless.append(mi);
1106             }
1107         }
1108         for (const auto mi : std::as_const(parentless)) {
1109             delete mi;
1110         }
1111 
1112         mUnassignedMessageListForPass3.clear();
1113         return;
1114     }
1115 
1116     // mUnassignedMessageListForPass3 is empty
1117     if (!mUnassignedMessageListForPass4.isEmpty()) {
1118         // we're in Pass4.. this is easy.
1119 
1120         // We have the same problem as in mUnassignedMessageListForPass2.
1121         QList<MessageItem *> parentless;
1122         for (const auto mi : std::as_const(mUnassignedMessageListForPass4)) {
1123             if (!mi->parent()) {
1124                 parentless.append(mi);
1125             }
1126         }
1127         for (const auto mi : std::as_const(parentless)) {
1128             delete mi;
1129         }
1130 
1131         mUnassignedMessageListForPass4.clear();
1132         return;
1133     }
1134 }
1135 
clearThreadingCacheReferencesIdMD5ToMessageItem()1136 void ModelPrivate::clearThreadingCacheReferencesIdMD5ToMessageItem()
1137 {
1138     qDeleteAll(mThreadingCacheMessageReferencesIdMD5ToMessageItem);
1139     mThreadingCacheMessageReferencesIdMD5ToMessageItem.clear();
1140 }
1141 
clearThreadingCacheMessageSubjectMD5ToMessageItem()1142 void ModelPrivate::clearThreadingCacheMessageSubjectMD5ToMessageItem()
1143 {
1144     qDeleteAll(mThreadingCacheMessageSubjectMD5ToMessageItem);
1145     mThreadingCacheMessageSubjectMD5ToMessageItem.clear();
1146 }
1147 
clearOrphanChildrenHash()1148 void ModelPrivate::clearOrphanChildrenHash()
1149 {
1150     qDeleteAll(mOrphanChildrenHash);
1151     mOrphanChildrenHash.clear();
1152 }
1153 
clearJobList()1154 void ModelPrivate::clearJobList()
1155 {
1156     if (mViewItemJobs.isEmpty()) {
1157         return;
1158     }
1159 
1160     if (mInLengthyJobBatch) {
1161         mInLengthyJobBatch = false;
1162     }
1163 
1164     qDeleteAll(mViewItemJobs);
1165     mViewItemJobs.clear();
1166 
1167     mModelForItemFunctions = q; // make sure it's true, as there remains no job with disconnected UI
1168 }
1169 
attachGroup(GroupHeaderItem * ghi)1170 void ModelPrivate::attachGroup(GroupHeaderItem *ghi)
1171 {
1172     if (ghi->parent()) {
1173         if (((ghi)->childItemCount() > 0) // has children
1174             && (ghi)->isViewable() // is actually attached to the viewable root
1175             && mModelForItemFunctions // the UI is not disconnected
1176             && mView->isExpanded(q->index(ghi, 0)) // is actually expanded
1177         ) {
1178             saveExpandedStateOfSubtree(ghi);
1179         }
1180 
1181         // FIXME: This *WILL* break selection and current index... :/
1182 
1183         ghi->parent()->takeChildItem(mModelForItemFunctions, ghi);
1184     }
1185 
1186     ghi->setParent(mRootItem);
1187 
1188     // I'm using a macro since it does really improve readability.
1189     // I'm NOT using a helper function since gcc will refuse to inline some of
1190     // the calls because they make this function grow too much.
1191 #define INSERT_GROUP_WITH_COMPARATOR(_ItemComparator)                                                                                                          \
1192     switch (mSortOrder->groupSortDirection()) {                                                                                                                \
1193     case SortOrder::Ascending:                                                                                                                                 \
1194         mRootItem->d_ptr->insertChildItem<_ItemComparator, true>(mModelForItemFunctions, ghi);                                                                 \
1195         break;                                                                                                                                                 \
1196     case SortOrder::Descending:                                                                                                                                \
1197         mRootItem->d_ptr->insertChildItem<_ItemComparator, false>(mModelForItemFunctions, ghi);                                                                \
1198         break;                                                                                                                                                 \
1199     default: /* should never happen... */                                                                                                                      \
1200         mRootItem->appendChildItem(mModelForItemFunctions, ghi);                                                                                               \
1201         break;                                                                                                                                                 \
1202     }
1203 
1204     switch (mSortOrder->groupSorting()) {
1205     case SortOrder::SortGroupsByDateTime:
1206         INSERT_GROUP_WITH_COMPARATOR(ItemDateComparator)
1207         break;
1208     case SortOrder::SortGroupsByDateTimeOfMostRecent:
1209         INSERT_GROUP_WITH_COMPARATOR(ItemMaxDateComparator)
1210         break;
1211     case SortOrder::SortGroupsBySenderOrReceiver:
1212         INSERT_GROUP_WITH_COMPARATOR(ItemSenderOrReceiverComparator)
1213         break;
1214     case SortOrder::SortGroupsBySender:
1215         INSERT_GROUP_WITH_COMPARATOR(ItemSenderComparator)
1216         break;
1217     case SortOrder::SortGroupsByReceiver:
1218         INSERT_GROUP_WITH_COMPARATOR(ItemReceiverComparator)
1219         break;
1220     case SortOrder::NoGroupSorting:
1221         mRootItem->appendChildItem(mModelForItemFunctions, ghi);
1222         break;
1223     default: // should never happen
1224         mRootItem->appendChildItem(mModelForItemFunctions, ghi);
1225         break;
1226     }
1227 
1228     if (ghi->initialExpandStatus() == Item::ExpandNeeded) { // this actually is a "non viewable expanded state"
1229         if (ghi->childItemCount() > 0) {
1230             if (mModelForItemFunctions) { // the UI is not disconnected
1231                 syncExpandedStateOfSubtree(ghi);
1232             }
1233         }
1234     }
1235 
1236     // A group header is always viewable, when attached: apply the filter, if we have it.
1237     if (mFilter) {
1238         Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected
1239         // apply the filter to subtree
1240         applyFilterToSubtree(ghi, QModelIndex());
1241     }
1242 }
1243 
saveExpandedStateOfSubtree(Item * root)1244 void ModelPrivate::saveExpandedStateOfSubtree(Item *root)
1245 {
1246     Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected here
1247     Q_ASSERT(root);
1248 
1249     root->setInitialExpandStatus(Item::ExpandNeeded);
1250 
1251     auto children = root->childItems();
1252     if (!children) {
1253         return;
1254     }
1255     for (const auto mi : std::as_const(*children)) {
1256         if (mi->childItemCount() > 0 // has children
1257             && mi->isViewable() // is actually attached to the viewable root
1258             && mView->isExpanded(q->index(mi, 0))) { // is actually expanded
1259             saveExpandedStateOfSubtree(mi);
1260         }
1261     }
1262 }
1263 
syncExpandedStateOfSubtree(Item * root)1264 void ModelPrivate::syncExpandedStateOfSubtree(Item *root)
1265 {
1266     Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected here
1267 
1268     // WE ASSUME that:
1269     // - the item is viewable
1270     // - its initialExpandStatus() is Item::ExpandNeeded
1271     // - 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)
1272 
1273     QModelIndex idx = q->index(root, 0);
1274 
1275     // if ( !mView->isExpanded( idx ) ) // this is O(logN!) in Qt.... very ugly... but it should never happen here
1276     mView->expand(idx); // sync the real state in the view
1277     root->setInitialExpandStatus(Item::ExpandExecuted);
1278 
1279     auto children = root->childItems();
1280     if (!children) {
1281         return;
1282     }
1283 
1284     for (const auto mi : std::as_const(*children)) {
1285         if (mi->initialExpandStatus() == Item::ExpandNeeded) {
1286             if (mi->childItemCount() > 0) {
1287                 syncExpandedStateOfSubtree(mi);
1288             }
1289         }
1290     }
1291 }
1292 
attachMessageToGroupHeader(MessageItem * mi)1293 void ModelPrivate::attachMessageToGroupHeader(MessageItem *mi)
1294 {
1295     QString groupLabel;
1296     time_t date;
1297 
1298     // compute the group header label and the date
1299     switch (mAggregation->grouping()) {
1300     case Aggregation::GroupByDate:
1301     case Aggregation::GroupByDateRange: {
1302         if (mAggregation->threadLeader() == Aggregation::MostRecentMessage) {
1303             date = mi->maxDate();
1304         } else {
1305             date = mi->date();
1306         }
1307 
1308         QDateTime dt;
1309         dt.setSecsSinceEpoch(date);
1310         QDate dDate = dt.date();
1311         int daysAgo = -1;
1312         const int daysInWeek = 7;
1313         if (dDate.isValid() && mTodayDate.isValid()) {
1314             daysAgo = dDate.daysTo(mTodayDate);
1315         }
1316 
1317         if ((daysAgo < 0) // In the future
1318             || (static_cast<uint>(date) == static_cast<uint>(-1))) { // Invalid
1319             groupLabel = mCachedUnknownLabel;
1320         } else if (daysAgo == 0) { // Today
1321             groupLabel = mCachedTodayLabel;
1322         } else if (daysAgo == 1) { // Yesterday
1323             groupLabel = mCachedYesterdayLabel;
1324         } else if (daysAgo > 1 && daysAgo < daysInWeek) { // Within last seven days
1325             auto dayName = mCachedDayNameLabel.find(dDate.dayOfWeek()); // non-const call, but non-shared container
1326             if (dayName == mCachedDayNameLabel.end()) {
1327                 dayName = mCachedDayNameLabel.insert(dDate.dayOfWeek(), QLocale::system().standaloneDayName(dDate.dayOfWeek()));
1328             }
1329             groupLabel = *dayName;
1330         } else if (mAggregation->grouping() == Aggregation::GroupByDate) { // GroupByDate seven days or more ago
1331             groupLabel = QLocale::system().toString(dDate, QLocale::ShortFormat);
1332         } else if (dDate.month() == mTodayDate.month() // GroupByDateRange within this month
1333                    && dDate.year() == mTodayDate.year()) {
1334             int startOfWeekDaysAgo = (daysInWeek + mTodayDate.dayOfWeek() - QLocale().firstDayOfWeek()) % daysInWeek;
1335             int weeksAgo = ((daysAgo - startOfWeekDaysAgo) / daysInWeek) + 1;
1336             switch (weeksAgo) {
1337             case 0: // This week
1338                 groupLabel = QLocale::system().standaloneDayName(dDate.dayOfWeek());
1339                 break;
1340             case 1: // 1 week ago
1341                 groupLabel = mCachedLastWeekLabel;
1342                 break;
1343             case 2:
1344                 groupLabel = mCachedTwoWeeksAgoLabel;
1345                 break;
1346             case 3:
1347                 groupLabel = mCachedThreeWeeksAgoLabel;
1348                 break;
1349             case 4:
1350                 groupLabel = mCachedFourWeeksAgoLabel;
1351                 break;
1352             case 5:
1353                 groupLabel = mCachedFiveWeeksAgoLabel;
1354                 break;
1355             default: // should never happen
1356                 groupLabel = mCachedUnknownLabel;
1357             }
1358         } else if (dDate.year() == mTodayDate.year()) { // GroupByDateRange within this year
1359             auto monthName = mCachedMonthNameLabel.find(dDate.month()); // non-const call, but non-shared container
1360             if (monthName == mCachedMonthNameLabel.end()) {
1361                 monthName = mCachedMonthNameLabel.insert(dDate.month(), QLocale::system().standaloneMonthName(dDate.month()));
1362             }
1363             groupLabel = *monthName;
1364         } else { // GroupByDateRange in previous years
1365             auto monthName = mCachedMonthNameLabel.find(dDate.month()); // non-const call, but non-shared container
1366             if (monthName == mCachedMonthNameLabel.end()) {
1367                 monthName = mCachedMonthNameLabel.insert(dDate.month(), QLocale::system().standaloneMonthName(dDate.month()));
1368             }
1369             groupLabel = i18nc("Message Aggregation Group Header: Month name and Year number",
1370                                "%1 %2",
1371                                *monthName,
1372                                QLocale::system().toString(dDate, QLatin1String("yyyy")));
1373         }
1374         break;
1375     }
1376 
1377     case Aggregation::GroupBySenderOrReceiver:
1378         date = mi->date();
1379         groupLabel = mi->displaySenderOrReceiver();
1380         break;
1381 
1382     case Aggregation::GroupBySender:
1383         date = mi->date();
1384         groupLabel = mi->displaySender();
1385         break;
1386 
1387     case Aggregation::GroupByReceiver:
1388         date = mi->date();
1389         groupLabel = mi->displayReceiver();
1390         break;
1391 
1392     case Aggregation::NoGrouping:
1393         // append directly to root
1394         attachMessageToParent(mRootItem, mi);
1395         return;
1396 
1397     default:
1398         // should never happen
1399         attachMessageToParent(mRootItem, mi);
1400         return;
1401     }
1402 
1403     GroupHeaderItem *ghi;
1404 
1405     ghi = mGroupHeaderItemHash.value(groupLabel, nullptr);
1406     if (!ghi) {
1407         // not found
1408 
1409         ghi = new GroupHeaderItem(groupLabel);
1410         ghi->initialSetup(date, mi->size(), mi->sender(), mi->receiver(), mi->useReceiver());
1411 
1412         switch (mAggregation->groupExpandPolicy()) {
1413         case Aggregation::NeverExpandGroups:
1414             // nothing to do
1415             break;
1416         case Aggregation::AlwaysExpandGroups:
1417             // expand always
1418             ghi->setInitialExpandStatus(Item::ExpandNeeded);
1419             break;
1420         case Aggregation::ExpandRecentGroups:
1421             // expand only if "close" to today
1422             if (mViewItemJobStepStartTime > ghi->date()) {
1423                 if ((mViewItemJobStepStartTime - ghi->date()) < (3600 * 72)) {
1424                     ghi->setInitialExpandStatus(Item::ExpandNeeded);
1425                 }
1426             } else {
1427                 if ((ghi->date() - mViewItemJobStepStartTime) < (3600 * 72)) {
1428                     ghi->setInitialExpandStatus(Item::ExpandNeeded);
1429                 }
1430             }
1431             break;
1432         default:
1433             // b0rken
1434             break;
1435         }
1436 
1437         attachMessageToParent(ghi, mi);
1438 
1439         attachGroup(ghi); // this will expand the group if required
1440 
1441         mGroupHeaderItemHash.insert(groupLabel, ghi);
1442     } else {
1443         // the group was already there (certainly viewable)
1444 
1445         // This function may be also called to re-group a message.
1446         // That is, to eventually find a new group for a message that has changed
1447         // its properties (but was already attached to a group).
1448         // So it may happen that we find out that in fact re-grouping wasn't really
1449         // needed because the message is already in the correct group.
1450         if (mi->parent() == ghi) {
1451             return; // nothing to be done
1452         }
1453 
1454         attachMessageToParent(ghi, mi);
1455     }
1456 
1457     // Remember this message as a thread leader
1458     mThreadingCache.updateParent(mi, nullptr);
1459 }
1460 
findMessageParent(MessageItem * mi)1461 MessageItem *ModelPrivate::findMessageParent(MessageItem *mi)
1462 {
1463     Q_ASSERT(mAggregation->threading() != Aggregation::NoThreading); // caller must take care of this
1464 
1465     // This function attempts to find a thread parent for the item "mi"
1466     // which actually may already have a children subtree.
1467 
1468     // Forged or plain broken message trees are dangerous here.
1469     // For example, a message tree with circular references like
1470     //
1471     //        Message mi, Id=1, In-Reply-To=2
1472     //          Message childOfMi, Id=2, In-Reply-To=1
1473     //
1474     // is perfectly possible and will cause us to find childOfMi
1475     // as parent of mi. This will then create a loop in the message tree
1476     // (which will then no longer be a tree in fact) and cause us to freeze
1477     // once we attempt to climb the parents. We need to take care of that.
1478 
1479     bool bMessageWasThreadable = false;
1480     MessageItem *pParent;
1481 
1482     // First of all try to find a "perfect parent", that is the message for that
1483     // we have the ID in the "In-Reply-To" field. This is actually done by using
1484     // MD5 caches of the message ids because of speed. Collisions are very unlikely.
1485 
1486     QByteArray md5 = mi->inReplyToIdMD5();
1487     if (!md5.isEmpty()) {
1488         // have an In-Reply-To field MD5
1489         pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr);
1490         if (pParent) {
1491             // Take care of circular references
1492             if ((mi == pParent) // self referencing message
1493                 || ((mi->childItemCount() > 0) // mi already has children, this is fast to determine
1494                     && pParent->hasAncestor(mi) // pParent is in the mi's children tree
1495                     )) {
1496                 qCWarning(MESSAGELIST_LOG) << "Circular In-Reply-To reference loop detected in the message tree";
1497                 mi->setThreadingStatus(MessageItem::NonThreadable);
1498                 return nullptr; // broken message: throw it away
1499             }
1500             mi->setThreadingStatus(MessageItem::PerfectParentFound);
1501             return pParent; // got a perfect parent for this message
1502         }
1503 
1504         // got no perfect parent
1505         bMessageWasThreadable = true; // but the message was threadable
1506     }
1507 
1508     if (mAggregation->threading() == Aggregation::PerfectOnly) {
1509         mi->setThreadingStatus(bMessageWasThreadable ? MessageItem::ParentMissing : MessageItem::NonThreadable);
1510         return nullptr; // we're doing only perfect parent matches
1511     }
1512 
1513     // Try to use the "References" field. In fact we have the MD5 of the
1514     // (n-1)th entry in References.
1515     //
1516     // Original rationale from KMHeaders:
1517     //
1518     // If we don't have a replyToId, or if we have one and the
1519     // corresponding message is not in this folder, as happens
1520     // if you keep your outgoing messages in an OUTBOX, for
1521     // example, try the list of references, because the second
1522     // to last will likely be in this folder. replyToAuxIdMD5
1523     // contains the second to last one.
1524 
1525     md5 = mi->referencesIdMD5();
1526     if (!md5.isEmpty()) {
1527         pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr);
1528         if (pParent) {
1529             // Take care of circular references
1530             if ((mi == pParent) // self referencing message
1531                 || ((mi->childItemCount() > 0) // mi already has children, this is fast to determine
1532                     && pParent->hasAncestor(mi) // pParent is in the mi's children tree
1533                     )) {
1534                 qCWarning(MESSAGELIST_LOG) << "Circular reference loop detected in the message tree";
1535                 mi->setThreadingStatus(MessageItem::NonThreadable);
1536                 return nullptr; // broken message: throw it away
1537             }
1538             mi->setThreadingStatus(MessageItem::ImperfectParentFound);
1539             return pParent; // got an imperfect parent for this message
1540         }
1541 
1542         auto messagesWithTheSameReferences = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(md5, nullptr);
1543         if (messagesWithTheSameReferences) {
1544             Q_ASSERT(!messagesWithTheSameReferences->isEmpty());
1545 
1546             pParent = messagesWithTheSameReferences->first();
1547             if (mi != pParent && (mi->childItemCount() == 0 || !pParent->hasAncestor(mi))) {
1548                 mi->setThreadingStatus(MessageItem::ImperfectParentFound);
1549                 return pParent;
1550             }
1551         }
1552 
1553         // got no imperfect parent
1554         bMessageWasThreadable = true; // but the message was threadable
1555     }
1556 
1557     if (mAggregation->threading() == Aggregation::PerfectAndReferences) {
1558         mi->setThreadingStatus(bMessageWasThreadable ? MessageItem::ParentMissing : MessageItem::NonThreadable);
1559         return nullptr; // we're doing only perfect parent matches
1560     }
1561 
1562     Q_ASSERT(mAggregation->threading() == Aggregation::PerfectReferencesAndSubject);
1563 
1564     // We are supposed to do subject based threading but we can't do it now.
1565     // This is because the subject based threading *may* be wrong and waste
1566     // time by creating circular references (that we'd need to detect and fix).
1567     // We first try the perfect and references based threading on all the messages
1568     // and then run subject based threading only on the remaining ones.
1569 
1570     mi->setThreadingStatus((bMessageWasThreadable || mi->subjectIsPrefixed()) ? MessageItem::ParentMissing : MessageItem::NonThreadable);
1571     return nullptr;
1572 }
1573 
1574 // Subject threading cache stuff
1575 
1576 #if 0
1577 // Debug helpers
1578 void dump_iterator_and_list(QList< MessageItem * >::Iterator &iter, QList< MessageItem * > *list)
1579 {
1580     qCDebug(MESSAGELIST_LOG) << "Threading cache part dump";
1581     if (iter == list->end()) {
1582         qCDebug(MESSAGELIST_LOG) << "Iterator pointing to end of the list";
1583     } else {
1584         qCDebug(MESSAGELIST_LOG) << "Iterator pointing to " << *iter << " subject [" << (*iter)->subject() << "] date [" << (*iter)->date() << "]";
1585     }
1586 
1587     for (QList< MessageItem * >::Iterator it = list->begin(); it != list->end(); ++it) {
1588         qCDebug(MESSAGELIST_LOG) << "List element " << *it << " subject [" << (*it)->subject() << "] date [" << (*it)->date() << "]";
1589     }
1590 
1591     qCDebug(MESSAGELIST_LOG) << "End of threading cache part dump";
1592 }
1593 
1594 void dump_list(QList< MessageItem * > *list)
1595 {
1596     qCDebug(MESSAGELIST_LOG) << "Threading cache part dump";
1597 
1598     for (QList< MessageItem * >::Iterator it = list->begin(); it != list->end(); ++it) {
1599         qCDebug(MESSAGELIST_LOG) << "List element " << *it << " subject [" << (*it)->subject() << "] date [" << (*it)->date() << "]";
1600     }
1601 
1602     qCDebug(MESSAGELIST_LOG) << "End of threading cache part dump";
1603 }
1604 
1605 #endif // debug helpers
1606 
1607 // a helper class used in a qLowerBound() call below
1608 class MessageLessThanByDate
1609 {
1610 public:
operator ()(const MessageItem * mi1,const MessageItem * mi2) const1611     inline bool operator()(const MessageItem *mi1, const MessageItem *mi2) const
1612     {
1613         if (mi1->date() < mi2->date()) { // likely
1614             return true;
1615         }
1616         if (mi1->date() > mi2->date()) { // likely
1617             return false;
1618         }
1619         // dates are equal, compare by pointer
1620         return mi1 < mi2;
1621     }
1622 };
1623 
addMessageToReferencesBasedThreadingCache(MessageItem * mi)1624 void ModelPrivate::addMessageToReferencesBasedThreadingCache(MessageItem *mi)
1625 {
1626     // Messages in this cache are sorted by date, and if dates are equal then they are sorted by pointer value.
1627     // Sorting by date is used to optimize the parent lookup in guessMessageParent() below.
1628 
1629     // WARNING: If the message date changes for some reason (like in the "update" step)
1630     //          then the cache may become unsorted. For this reason the message about to
1631     //          be changed must be first removed from the cache and then reinserted.
1632 
1633     auto messagesWithTheSameReference = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(mi->referencesIdMD5(), nullptr);
1634 
1635     if (!messagesWithTheSameReference) {
1636         messagesWithTheSameReference = new QList<MessageItem *>();
1637         mThreadingCacheMessageReferencesIdMD5ToMessageItem.insert(mi->referencesIdMD5(), messagesWithTheSameReference);
1638         messagesWithTheSameReference->append(mi);
1639         return;
1640     }
1641 
1642     // Found: assert that we have no duplicates in the cache.
1643     Q_ASSERT(!messagesWithTheSameReference->contains(mi));
1644 
1645     // Ordered insert: first by date then by pointer value.
1646     auto it = std::lower_bound(messagesWithTheSameReference->begin(), messagesWithTheSameReference->end(), mi, MessageLessThanByDate());
1647     messagesWithTheSameReference->insert(it, mi);
1648 }
1649 
removeMessageFromReferencesBasedThreadingCache(MessageItem * mi)1650 void ModelPrivate::removeMessageFromReferencesBasedThreadingCache(MessageItem *mi)
1651 {
1652     // We assume that the caller knows what he is doing and the message is actually in the cache.
1653     // If the message isn't in the cache then we should not be called at all.
1654 
1655     auto messagesWithTheSameReference = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(mi->referencesIdMD5(), nullptr);
1656 
1657     // We assume that the message is there so the list must be non null.
1658     Q_ASSERT(messagesWithTheSameReference);
1659 
1660     // The cache *MUST* be ordered first by date then by pointer value
1661     auto it = std::lower_bound(messagesWithTheSameReference->begin(), messagesWithTheSameReference->end(), mi, MessageLessThanByDate());
1662 
1663     // The binary based search must have found a message
1664     Q_ASSERT(it != messagesWithTheSameReference->end());
1665 
1666     // and it must have found exactly the message requested
1667     Q_ASSERT(*it == mi);
1668 
1669     // Kill it
1670     messagesWithTheSameReference->erase(it);
1671 
1672     // And kill the list if it was the last one
1673     if (messagesWithTheSameReference->isEmpty()) {
1674         mThreadingCacheMessageReferencesIdMD5ToMessageItem.remove(mi->referencesIdMD5());
1675         delete messagesWithTheSameReference;
1676     }
1677 }
1678 
addMessageToSubjectBasedThreadingCache(MessageItem * mi)1679 void ModelPrivate::addMessageToSubjectBasedThreadingCache(MessageItem *mi)
1680 {
1681     // Messages in this cache are sorted by date, and if dates are equal then they are sorted by pointer value.
1682     // Sorting by date is used to optimize the parent lookup in guessMessageParent() below.
1683 
1684     // WARNING: If the message date changes for some reason (like in the "update" step)
1685     //          then the cache may become unsorted. For this reason the message about to
1686     //          be changed must be first removed from the cache and then reinserted.
1687 
1688     // Lookup the list of messages with the same stripped subject
1689     auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(mi->strippedSubjectMD5(), nullptr);
1690 
1691     if (!messagesWithTheSameStrippedSubject) {
1692         // Not there yet: create it and append.
1693         messagesWithTheSameStrippedSubject = new QList<MessageItem *>();
1694         mThreadingCacheMessageSubjectMD5ToMessageItem.insert(mi->strippedSubjectMD5(), messagesWithTheSameStrippedSubject);
1695         messagesWithTheSameStrippedSubject->append(mi);
1696         return;
1697     }
1698 
1699     // Found: assert that we have no duplicates in the cache.
1700     Q_ASSERT(!messagesWithTheSameStrippedSubject->contains(mi));
1701 
1702     // Ordered insert: first by date then by pointer value.
1703     auto it = std::lower_bound(messagesWithTheSameStrippedSubject->begin(), messagesWithTheSameStrippedSubject->end(), mi, MessageLessThanByDate());
1704     messagesWithTheSameStrippedSubject->insert(it, mi);
1705 }
1706 
removeMessageFromSubjectBasedThreadingCache(MessageItem * mi)1707 void ModelPrivate::removeMessageFromSubjectBasedThreadingCache(MessageItem *mi)
1708 {
1709     // We assume that the caller knows what he is doing and the message is actually in the cache.
1710     // If the message isn't in the cache then we should not be called at all.
1711     //
1712     // The game is called "performance"
1713 
1714     // Grab the list of all the messages with the same stripped subject (all potential parents)
1715     auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(mi->strippedSubjectMD5(), nullptr);
1716 
1717     // We assume that the message is there so the list must be non null.
1718     Q_ASSERT(messagesWithTheSameStrippedSubject);
1719 
1720     // The cache *MUST* be ordered first by date then by pointer value
1721     auto it = std::lower_bound(messagesWithTheSameStrippedSubject->begin(), messagesWithTheSameStrippedSubject->end(), mi, MessageLessThanByDate());
1722 
1723     // The binary based search must have found a message
1724     Q_ASSERT(it != messagesWithTheSameStrippedSubject->end());
1725 
1726     // and it must have found exactly the message requested
1727     Q_ASSERT(*it == mi);
1728 
1729     // Kill it
1730     messagesWithTheSameStrippedSubject->erase(it);
1731 
1732     // And kill the list if it was the last one
1733     if (messagesWithTheSameStrippedSubject->isEmpty()) {
1734         mThreadingCacheMessageSubjectMD5ToMessageItem.remove(mi->strippedSubjectMD5());
1735         delete messagesWithTheSameStrippedSubject;
1736     }
1737 }
1738 
guessMessageParent(MessageItem * mi)1739 MessageItem *ModelPrivate::guessMessageParent(MessageItem *mi)
1740 {
1741     // This function implements subject based threading
1742     // It attempts to guess a thread parent for the item "mi"
1743     // which actually may already have a children subtree.
1744 
1745     // We have all the problems of findMessageParent() plus the fact that
1746     // we're actually guessing (and often we may be *wrong*).
1747 
1748     Q_ASSERT(mAggregation->threading() == Aggregation::PerfectReferencesAndSubject); // caller must take care of this
1749     Q_ASSERT(mi->subjectIsPrefixed()); // caller must take care of this
1750     Q_ASSERT(mi->threadingStatus() == MessageItem::ParentMissing);
1751 
1752     // Do subject based threading
1753     const QByteArray md5 = mi->strippedSubjectMD5();
1754     if (!md5.isEmpty()) {
1755         auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(md5, nullptr);
1756 
1757         if (messagesWithTheSameStrippedSubject) {
1758             Q_ASSERT(!messagesWithTheSameStrippedSubject->isEmpty());
1759 
1760             // Need to find the message with the maximum date lower than the one of this message
1761 
1762             auto maxTime = (time_t)0;
1763             MessageItem *pParent = nullptr;
1764 
1765             // Here'we re really guessing so circular references are possible
1766             // even on perfectly valid trees. This is why we don't consider it
1767             // an error but just continue searching.
1768 
1769             // FIXME: This might be speed up with an initial binary search (?)
1770             // ANSWER: No. We can't rely on date order (as it can be updated on the fly...)
1771             for (const auto it : std::as_const(*messagesWithTheSameStrippedSubject)) {
1772                 int delta = mi->date() - it->date();
1773 
1774                 // We don't take into account messages with a delta smaller than 120.
1775                 // Assuming that our date() values are correct (that is, they take into
1776                 // account timezones etc..) then one usually needs more than 120 seconds
1777                 // to answer to a message. Better safe than sorry.
1778 
1779                 // This check also includes negative deltas so messages later than mi aren't considered
1780 
1781                 if (delta < 120) {
1782                     break; // The list is ordered by date (ascending) so we can stop searching here
1783                 }
1784 
1785                 // About the "magic" 3628899 value here comes a Till's comment from the original KMHeaders:
1786                 //
1787                 //   "Parents more than six weeks older than the message are not accepted. The reasoning being
1788                 //   that if a new message with the same subject turns up after such a long time, the chances
1789                 //   that it is still part of the same thread are slim. The value of six weeks is chosen as a
1790                 //   result of a poll conducted on kde-devel, so it's probably bogus. :)"
1791 
1792                 if (delta < 3628899) {
1793                     // Compute the closest.
1794                     if ((maxTime < it->date())) {
1795                         // This algorithm *can* be (and often is) wrong.
1796                         // Take care of circular threading which is really possible at this level.
1797                         // If mi contains "it" inside its children subtree then we have
1798                         // found such a circular threading problem.
1799 
1800                         // Note that here we can't have it == mi because of the delta >= 120 check above.
1801 
1802                         if ((mi->childItemCount() == 0) || !it->hasAncestor(mi)) {
1803                             maxTime = it->date();
1804                             pParent = it;
1805                         }
1806                     }
1807                 }
1808             }
1809 
1810             if (pParent) {
1811                 mi->setThreadingStatus(MessageItem::ImperfectParentFound);
1812                 return pParent; // got an imperfect parent for this message
1813             }
1814         }
1815     }
1816 
1817     return nullptr;
1818 }
1819 
1820 //
1821 // A little template helper, hopefully inlineable.
1822 //
1823 // Return true if the specified message item is in the wrong position
1824 // inside the specified parent and needs re-sorting. Return false otherwise.
1825 // Both parent and messageItem must not be null.
1826 //
1827 // Checking if a message needs re-sorting instead of just re-sorting it
1828 // is very useful since re-sorting is an expensive operation.
1829 //
1830 template<class ItemComparator>
messageItemNeedsReSorting(SortOrder::SortDirection messageSortDirection,ItemPrivate * parent,MessageItem * messageItem)1831 static bool messageItemNeedsReSorting(SortOrder::SortDirection messageSortDirection, ItemPrivate *parent, MessageItem *messageItem)
1832 {
1833     if ((messageSortDirection == SortOrder::Ascending) || (parent->mType == Item::Message)) {
1834         return parent->childItemNeedsReSorting<ItemComparator, true>(messageItem);
1835     }
1836     return parent->childItemNeedsReSorting<ItemComparator, false>(messageItem);
1837 }
1838 
handleItemPropertyChanges(int propertyChangeMask,Item * parent,Item * item)1839 bool ModelPrivate::handleItemPropertyChanges(int propertyChangeMask, Item *parent, Item *item)
1840 {
1841     // The facts:
1842     //
1843     // - If dates changed:
1844     //   - If we're sorting messages by min/max date then at each level the messages might need resorting.
1845     //   - If the thread leader is the most recent message of a thread then the uppermost
1846     //     message of the thread might need re-grouping.
1847     //   - If the groups are sorted by min/max date then the group might need re-sorting too.
1848     //
1849     // This function explicitly doesn't re-apply the filter when ActionItemStatus changes.
1850     // This is because filters must be re-applied due to a broader range of status variations:
1851     // this is done in viewItemJobStepInternalForJobPass1Update() instead (which is the only
1852     // place in that ActionItemStatus may be set).
1853 
1854     if (parent->type() == Item::InvisibleRoot) {
1855         // item is either a message or a group attached to the root.
1856         // It might need resorting.
1857         if (item->type() == Item::GroupHeader) {
1858             // item is a group header attached to the root.
1859             if ((
1860                     // max date changed
1861                     (propertyChangeMask & MaxDateChanged) && // groups sorted by max date
1862                     (mSortOrder->groupSorting() == SortOrder::SortGroupsByDateTimeOfMostRecent))
1863                 || (
1864                     // date changed
1865                     (propertyChangeMask & DateChanged) && // groups sorted by date
1866                     (mSortOrder->groupSorting() == SortOrder::SortGroupsByDateTime))) {
1867                 // This group might need re-sorting.
1868 
1869                 // Groups are large container of messages so it's likely that
1870                 // another message inserted will cause this group to be marked again.
1871                 // So we wait until the end to do the grand final re-sorting: it will be done in Pass4.
1872                 mGroupHeadersThatNeedUpdate.insert(static_cast<GroupHeaderItem *>(item), static_cast<GroupHeaderItem *>(item));
1873             }
1874         } else {
1875             // item is a message. It might need re-sorting.
1876 
1877             // Since sorting is an expensive operation, we first check if it's *really* needed.
1878             // Re-sorting will actually not change min/max dates at all and
1879             // will not climb up the parent's ancestor tree.
1880 
1881             switch (mSortOrder->messageSorting()) {
1882             case SortOrder::SortMessagesByDateTime:
1883                 if (propertyChangeMask & DateChanged) { // date changed
1884                     if (messageItemNeedsReSorting<ItemDateComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) {
1885                         attachMessageToParent(parent, static_cast<MessageItem *>(item));
1886                     }
1887                 } // else date changed, but it doesn't match sorting order: no need to re-sort
1888                 break;
1889             case SortOrder::SortMessagesByDateTimeOfMostRecent:
1890                 if (propertyChangeMask & MaxDateChanged) { // max date changed
1891                     if (messageItemNeedsReSorting<ItemMaxDateComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) {
1892                         attachMessageToParent(parent, static_cast<MessageItem *>(item));
1893                     }
1894                 } // else max date changed, but it doesn't match sorting order: no need to re-sort
1895                 break;
1896             case SortOrder::SortMessagesByActionItemStatus:
1897                 if (propertyChangeMask & ActionItemStatusChanged) { // todo status changed
1898                     if (messageItemNeedsReSorting<ItemActionItemStatusComparator>(mSortOrder->messageSortDirection(),
1899                                                                                   parent->d_ptr,
1900                                                                                   static_cast<MessageItem *>(item))) {
1901                         attachMessageToParent(parent, static_cast<MessageItem *>(item));
1902                     }
1903                 } // else to do status changed, but it doesn't match sorting order: no need to re-sort
1904                 break;
1905             case SortOrder::SortMessagesByUnreadStatus:
1906                 if (propertyChangeMask & UnreadStatusChanged) { // new / unread status changed
1907                     if (messageItemNeedsReSorting<ItemUnreadStatusComparator>(mSortOrder->messageSortDirection(),
1908                                                                               parent->d_ptr,
1909                                                                               static_cast<MessageItem *>(item))) {
1910                         attachMessageToParent(parent, static_cast<MessageItem *>(item));
1911                     }
1912                 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort
1913                 break;
1914             case SortOrder::SortMessagesByImportantStatus:
1915                 if (propertyChangeMask & ImportantStatusChanged) { // important status changed
1916                     if (messageItemNeedsReSorting<ItemImportantStatusComparator>(mSortOrder->messageSortDirection(),
1917                                                                                  parent->d_ptr,
1918                                                                                  static_cast<MessageItem *>(item))) {
1919                         attachMessageToParent(parent, static_cast<MessageItem *>(item));
1920                     }
1921                 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort
1922                 break;
1923             case SortOrder::SortMessagesByAttachmentStatus:
1924                 if (propertyChangeMask & AttachmentStatusChanged) { // attachment status changed
1925                     if (messageItemNeedsReSorting<ItemAttachmentStatusComparator>(mSortOrder->messageSortDirection(),
1926                                                                                   parent->d_ptr,
1927                                                                                   static_cast<MessageItem *>(item))) {
1928                         attachMessageToParent(parent, static_cast<MessageItem *>(item));
1929                     }
1930                 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort
1931                 break;
1932             default:
1933                 // this kind of message sorting isn't affected by the property changes: nothing to do.
1934                 break;
1935             }
1936         }
1937 
1938         return false; // the invisible root isn't affected by any change.
1939     }
1940 
1941     if (parent->type() == Item::GroupHeader) {
1942         // item is a message attached to a GroupHeader.
1943         // It might need re-grouping or re-sorting (within the same group)
1944 
1945         // Check re-grouping here.
1946         if ((
1947                 // max date changed
1948                 (propertyChangeMask & MaxDateChanged) && // thread leader is most recent message
1949                 (mAggregation->threadLeader() == Aggregation::MostRecentMessage))
1950             || (
1951                 // date changed
1952                 (propertyChangeMask & DateChanged) && // thread leader the topmost message
1953                 (mAggregation->threadLeader() == Aggregation::TopmostMessage))) {
1954             // Might really need re-grouping.
1955             // attachMessageToGroupHeader() will find the right group for this message
1956             // and if it's different than the current it will move it.
1957             attachMessageToGroupHeader(static_cast<MessageItem *>(item));
1958             // Re-grouping fixes the properties of the involved group headers
1959             // so at exit of attachMessageToGroupHeader() the parent can't be affected
1960             // by the change anymore.
1961             return false;
1962         }
1963 
1964         // Re-grouping wasn't needed. Re-sorting might be.
1965     } // else item is a message attached to another message and might need re-sorting only.
1966 
1967     // Check if message needs re-sorting.
1968 
1969     switch (mSortOrder->messageSorting()) {
1970     case SortOrder::SortMessagesByDateTime:
1971         if (propertyChangeMask & DateChanged) { // date changed
1972             if (messageItemNeedsReSorting<ItemDateComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) {
1973                 attachMessageToParent(parent, static_cast<MessageItem *>(item));
1974             }
1975         } // else date changed, but it doesn't match sorting order: no need to re-sort
1976         break;
1977     case SortOrder::SortMessagesByDateTimeOfMostRecent:
1978         if (propertyChangeMask & MaxDateChanged) { // max date changed
1979             if (messageItemNeedsReSorting<ItemMaxDateComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) {
1980                 attachMessageToParent(parent, static_cast<MessageItem *>(item));
1981             }
1982         } // else max date changed, but it doesn't match sorting order: no need to re-sort
1983         break;
1984     case SortOrder::SortMessagesByActionItemStatus:
1985         if (propertyChangeMask & ActionItemStatusChanged) { // todo status changed
1986             if (messageItemNeedsReSorting<ItemActionItemStatusComparator>(mSortOrder->messageSortDirection(),
1987                                                                           parent->d_ptr,
1988                                                                           static_cast<MessageItem *>(item))) {
1989                 attachMessageToParent(parent, static_cast<MessageItem *>(item));
1990             }
1991         } // else to do status changed, but it doesn't match sorting order: no need to re-sort
1992         break;
1993     case SortOrder::SortMessagesByUnreadStatus:
1994         if (propertyChangeMask & UnreadStatusChanged) { // new / unread status changed
1995             if (messageItemNeedsReSorting<ItemUnreadStatusComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) {
1996                 attachMessageToParent(parent, static_cast<MessageItem *>(item));
1997             }
1998         } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort
1999         break;
2000     case SortOrder::SortMessagesByImportantStatus:
2001         if (propertyChangeMask & ImportantStatusChanged) { // important status changed
2002             if (messageItemNeedsReSorting<ItemImportantStatusComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) {
2003                 attachMessageToParent(parent, static_cast<MessageItem *>(item));
2004             }
2005         } // else important status changed, but it doesn't match sorting order: no need to re-sort
2006         break;
2007     case SortOrder::SortMessagesByAttachmentStatus:
2008         if (propertyChangeMask & AttachmentStatusChanged) { // attachment status changed
2009             if (messageItemNeedsReSorting<ItemAttachmentStatusComparator>(mSortOrder->messageSortDirection(),
2010                                                                           parent->d_ptr,
2011                                                                           static_cast<MessageItem *>(item))) {
2012                 attachMessageToParent(parent, static_cast<MessageItem *>(item));
2013             }
2014         } // else important status changed, but it doesn't match sorting order: no need to re-sort
2015         break;
2016     default:
2017         // this kind of message sorting isn't affected by property changes: nothing to do.
2018         break;
2019     }
2020 
2021     return true; // parent might be affected too.
2022 }
2023 
messageDetachedUpdateParentProperties(Item * oldParent,MessageItem * mi)2024 void ModelPrivate::messageDetachedUpdateParentProperties(Item *oldParent, MessageItem *mi)
2025 {
2026     Q_ASSERT(oldParent);
2027     Q_ASSERT(mi);
2028     Q_ASSERT(oldParent != mRootItem);
2029 
2030     // oldParent might have its properties changed because of the child removal.
2031     // propagate the changes up.
2032     for (;;) {
2033         // pParent is not the root item now. This is assured by how we enter this loop
2034         // and by the fact that handleItemPropertyChanges returns false when grandParent
2035         // is Item::InvisibleRoot. We could actually assert it here...
2036 
2037         // Check if its dates need an update.
2038         int propertyChangeMask;
2039 
2040         if ((mi->maxDate() == oldParent->maxDate()) && oldParent->recomputeMaxDate()) {
2041             propertyChangeMask = MaxDateChanged;
2042         } else {
2043             break; // from the for(;;) loop
2044         }
2045 
2046         // One of the oldParent properties has changed for sure
2047 
2048         Item *grandParent = oldParent->parent();
2049 
2050         // If there is no grandParent then oldParent isn't attached to the view.
2051         // Re-sorting / re-grouping isn't needed for sure.
2052         if (!grandParent) {
2053             break; // from the for(;;) loop
2054         }
2055 
2056         // The following function will return true if grandParent may be affected by the change.
2057         // If the grandParent isn't affected, we stop climbing.
2058         if (!handleItemPropertyChanges(propertyChangeMask, grandParent, oldParent)) {
2059             break; // from the for(;;) loop
2060         }
2061 
2062         // Now we need to climb up one level and check again.
2063         oldParent = grandParent;
2064     } // for(;;) loop
2065 
2066     // If the last message was removed from a group header then this group will need an update
2067     // for sure. We will need to remove it (unless a message is attached back to it)
2068     if (oldParent->type() == Item::GroupHeader) {
2069         if (oldParent->childItemCount() == 0) {
2070             mGroupHeadersThatNeedUpdate.insert(static_cast<GroupHeaderItem *>(oldParent), static_cast<GroupHeaderItem *>(oldParent));
2071         }
2072     }
2073 }
2074 
propagateItemPropertiesToParent(Item * item)2075 void ModelPrivate::propagateItemPropertiesToParent(Item *item)
2076 {
2077     Item *pParent = item->parent();
2078     Q_ASSERT(pParent);
2079     Q_ASSERT(pParent != mRootItem);
2080 
2081     for (;;) {
2082         // pParent is not the root item now. This is assured by how we enter this loop
2083         // and by the fact that handleItemPropertyChanges returns false when grandParent
2084         // is Item::InvisibleRoot. We could actually assert it here...
2085 
2086         // Check if its dates need an update.
2087         int propertyChangeMask;
2088 
2089         if (item->maxDate() > pParent->maxDate()) {
2090             pParent->setMaxDate(item->maxDate());
2091             propertyChangeMask = MaxDateChanged;
2092         } else {
2093             // No parent dates have changed: no further work is needed. Stop climbing here.
2094             break; // from the for(;;) loop
2095         }
2096 
2097         // One of the pParent properties has changed.
2098 
2099         Item *grandParent = pParent->parent();
2100 
2101         // If there is no grandParent then pParent isn't attached to the view.
2102         // Re-sorting / re-grouping isn't needed for sure.
2103         if (!grandParent) {
2104             break; // from the for(;;) loop
2105         }
2106 
2107         // The following function will return true if grandParent may be affected by the change.
2108         // If the grandParent isn't affected, we stop climbing.
2109         if (!handleItemPropertyChanges(propertyChangeMask, grandParent, pParent)) {
2110             break; // from the for(;;) loop
2111         }
2112 
2113         // Now we need to climb up one level and check again.
2114         pParent = grandParent;
2115     } // for(;;)
2116 }
2117 
attachMessageToParent(Item * pParent,MessageItem * mi,AttachOptions attachOptions)2118 void ModelPrivate::attachMessageToParent(Item *pParent, MessageItem *mi, AttachOptions attachOptions)
2119 {
2120     Q_ASSERT(pParent);
2121     Q_ASSERT(mi);
2122 
2123     // This function may be called to do a simple "re-sort" of the item inside the parent.
2124     // In that case mi->parent() is equal to pParent.
2125     bool oldParentWasTheSame;
2126 
2127     if (mi->parent()) {
2128         Item *oldParent = mi->parent();
2129 
2130         // The item already had a parent and this means that we're moving it.
2131         oldParentWasTheSame = oldParent == pParent; // just re-sorting ?
2132 
2133         if (mi->isViewable()) { // is actually
2134             // The message is actually attached to the viewable root
2135 
2136             // Unfortunately we need to hack the model/view architecture
2137             // since it's somewhat flawed in this. At the moment of writing
2138             // there is simply no way to atomically move a subtree.
2139             // We must detach, call beginRemoveRows()/endRemoveRows(),
2140             // save the expanded state, save the selection, save the current item,
2141             // save the view position (YES! As we are removing items the view
2142             // will hopelessly jump around so we're just FORCED to break
2143             // the isolation from the view)...
2144             // ...*then* reattach, restore the expanded state, restore the selection,
2145             // restore the current item, restore the view position and pray
2146             // that nothing will fail in the (rather complicated) process....
2147 
2148             // Yet more unfortunately, while saving the expanded state might stop
2149             // at a certain (unexpanded) point in the tree, saving the selection
2150             // is hopelessly recursive down to the bare leafs.
2151 
2152             // Furthermore the expansion of items is a common case while selection
2153             // in the subtree is rare, so saving it would be a huge cost with
2154             // a low revenue.
2155 
2156             // This is why we just let the selection screw up. I hereby refuse to call
2157             // yet another expensive recursive function here :D
2158 
2159             // The current item saving can be somewhat optimized doing it once for
2160             // a single job step...
2161 
2162             if (((mi)->childItemCount() > 0) // has children
2163                 && mModelForItemFunctions // the UI is not actually disconnected
2164                 && mView->isExpanded(q->index(mi, 0)) // is actually expanded
2165             ) {
2166                 saveExpandedStateOfSubtree(mi);
2167             }
2168         }
2169 
2170         // If the parent is viewable (so mi was viewable too) then the beginRemoveRows()
2171         // and endRemoveRows() functions of this model will be called too.
2172         oldParent->takeChildItem(mModelForItemFunctions, mi);
2173 
2174         if ((!oldParentWasTheSame) && (oldParent != mRootItem)) {
2175             messageDetachedUpdateParentProperties(oldParent, mi);
2176         }
2177     } else {
2178         // The item had no parent yet.
2179         oldParentWasTheSame = false;
2180     }
2181 
2182     // Take care of perfect / imperfect threading.
2183     // Items that are now perfectly threaded, but already have a different parent
2184     // might have been imperfectly threaded before. Remove them from the caches.
2185     // Items that are now imperfectly threaded must be added to the caches.
2186     //
2187     // If we're just re-sorting the item inside the same parent then the threading
2188     // caches don't need to be updated (since they actually depend on the parent).
2189 
2190     if (!oldParentWasTheSame) {
2191         switch (mi->threadingStatus()) {
2192         case MessageItem::PerfectParentFound:
2193             if (!mi->inReplyToIdMD5().isEmpty()) {
2194                 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.remove(mi->inReplyToIdMD5(), mi);
2195             }
2196             if (attachOptions == StoreInCache && pParent->type() == Item::Message) {
2197                 mThreadingCache.updateParent(mi, static_cast<MessageItem *>(pParent));
2198             }
2199             break;
2200         case MessageItem::ImperfectParentFound:
2201         case MessageItem::ParentMissing: // may be: temporary or just fallback assignment
2202             if (!mi->inReplyToIdMD5().isEmpty()) {
2203                 if (!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(mi->inReplyToIdMD5(), mi)) {
2204                     mThreadingCacheMessageInReplyToIdMD5ToMessageItem.insert(mi->inReplyToIdMD5(), mi);
2205                 }
2206             }
2207             break;
2208         case MessageItem::NonThreadable: // this also happens when we do no threading at all
2209             // make gcc happy
2210             Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(mi->inReplyToIdMD5(), mi));
2211             break;
2212         }
2213     }
2214 
2215     // Set the new parent
2216     mi->setParent(pParent);
2217 
2218     // Propagate watched and ignored status
2219     if ((pParent->status().toQInt32() & mCachedWatchedOrIgnoredStatusBits) // unlikely
2220         && (pParent->type() == Item::Message) // likely
2221     ) {
2222         // the parent is either watched or ignored: propagate to the child
2223         if (pParent->status().isWatched()) {
2224             int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(mi);
2225             mi->setStatus(Akonadi::MessageStatus::statusWatched());
2226             mStorageModel->setMessageItemStatus(mi, row, Akonadi::MessageStatus::statusWatched());
2227         } else if (pParent->status().isIgnored()) {
2228             int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(mi);
2229             mi->setStatus(Akonadi::MessageStatus::statusIgnored());
2230             mStorageModel->setMessageItemStatus(mi, row, Akonadi::MessageStatus::statusIgnored());
2231         }
2232     }
2233 
2234     // And insert into its child list
2235 
2236     // If pParent is viewable then the insert/append functions will call this model's
2237     // beginInsertRows() and endInsertRows() functions. This is EXTREMELY
2238     // expensive and ugly but it's the only way with the Qt4 imposed Model/View method.
2239     // Dude... (citation from Lost, if it wasn't clear).
2240 
2241     // I'm using a macro since it does really improve readability.
2242     // I'm NOT using a helper function since gcc will refuse to inline some of
2243     // the calls because they make this function grow too much.
2244 #define INSERT_MESSAGE_WITH_COMPARATOR(_ItemComparator)                                                                                                        \
2245     if ((mSortOrder->messageSortDirection() == SortOrder::Ascending) || (pParent->type() == Item::Message)) {                                                  \
2246         pParent->d_ptr->insertChildItem<_ItemComparator, true>(mModelForItemFunctions, mi);                                                                    \
2247     } else {                                                                                                                                                   \
2248         pParent->d_ptr->insertChildItem<_ItemComparator, false>(mModelForItemFunctions, mi);                                                                   \
2249     }
2250 
2251     // If pParent is viewable then the insertion call will also set the child state to viewable.
2252     // Since mi MAY have children, then this call may make them viewable.
2253     switch (mSortOrder->messageSorting()) {
2254     case SortOrder::SortMessagesByDateTime:
2255         INSERT_MESSAGE_WITH_COMPARATOR(ItemDateComparator)
2256         break;
2257     case SortOrder::SortMessagesByDateTimeOfMostRecent:
2258         INSERT_MESSAGE_WITH_COMPARATOR(ItemMaxDateComparator)
2259         break;
2260     case SortOrder::SortMessagesBySize:
2261         INSERT_MESSAGE_WITH_COMPARATOR(ItemSizeComparator)
2262         break;
2263     case SortOrder::SortMessagesBySenderOrReceiver:
2264         INSERT_MESSAGE_WITH_COMPARATOR(ItemSenderOrReceiverComparator)
2265         break;
2266     case SortOrder::SortMessagesBySender:
2267         INSERT_MESSAGE_WITH_COMPARATOR(ItemSenderComparator)
2268         break;
2269     case SortOrder::SortMessagesByReceiver:
2270         INSERT_MESSAGE_WITH_COMPARATOR(ItemReceiverComparator)
2271         break;
2272     case SortOrder::SortMessagesBySubject:
2273         INSERT_MESSAGE_WITH_COMPARATOR(ItemSubjectComparator)
2274         break;
2275     case SortOrder::SortMessagesByActionItemStatus:
2276         INSERT_MESSAGE_WITH_COMPARATOR(ItemActionItemStatusComparator)
2277         break;
2278     case SortOrder::SortMessagesByUnreadStatus:
2279         INSERT_MESSAGE_WITH_COMPARATOR(ItemUnreadStatusComparator)
2280         break;
2281     case SortOrder::SortMessagesByImportantStatus:
2282         INSERT_MESSAGE_WITH_COMPARATOR(ItemImportantStatusComparator)
2283         break;
2284     case SortOrder::SortMessagesByAttachmentStatus:
2285         INSERT_MESSAGE_WITH_COMPARATOR(ItemAttachmentStatusComparator)
2286         break;
2287     case SortOrder::NoMessageSorting:
2288         pParent->appendChildItem(mModelForItemFunctions, mi);
2289         break;
2290     default: // should never happen
2291         pParent->appendChildItem(mModelForItemFunctions, mi);
2292         break;
2293     }
2294 
2295     // Decide if we need to expand parents
2296     bool childNeedsExpanding = (mi->initialExpandStatus() == Item::ExpandNeeded);
2297 
2298     if (pParent->initialExpandStatus() == Item::NoExpandNeeded) {
2299         switch (mAggregation->threadExpandPolicy()) {
2300         case Aggregation::NeverExpandThreads:
2301             // just do nothing unless this child has children and is already marked for expansion
2302             if (childNeedsExpanding) {
2303                 pParent->setInitialExpandStatus(Item::ExpandNeeded);
2304             }
2305             break;
2306         case Aggregation::ExpandThreadsWithNewMessages: // No more new status. fall through to unread if it exists in config
2307         case Aggregation::ExpandThreadsWithUnreadMessages:
2308             // expand only if unread (or it has children marked for expansion)
2309             if (childNeedsExpanding || !mi->status().isRead()) {
2310                 pParent->setInitialExpandStatus(Item::ExpandNeeded);
2311             }
2312             break;
2313         case Aggregation::ExpandThreadsWithUnreadOrImportantMessages:
2314             // expand only if unread, important or todo (or it has children marked for expansion)
2315             // FIXME: Wouldn't it be nice to be able to test for bitmasks in MessageStatus ?
2316             if (childNeedsExpanding || !mi->status().isRead() || mi->status().isImportant() || mi->status().isToAct()) {
2317                 pParent->setInitialExpandStatus(Item::ExpandNeeded);
2318             }
2319             break;
2320         case Aggregation::AlwaysExpandThreads:
2321             // expand everything
2322             pParent->setInitialExpandStatus(Item::ExpandNeeded);
2323             break;
2324         default:
2325             // BUG
2326             break;
2327         }
2328     } // else it's already marked for expansion or expansion has been already executed
2329 
2330     // expand parent first, if possible
2331     if (pParent->initialExpandStatus() == Item::ExpandNeeded) {
2332         // If UI is not disconnected and parent is viewable, go up and expand
2333         if (mModelForItemFunctions && pParent->isViewable()) {
2334             // Now expand parents as needed
2335             Item *parentToExpand = pParent;
2336             while (parentToExpand) {
2337                 if (parentToExpand == mRootItem) {
2338                     break; // no need to set it expanded
2339                 }
2340                 // parentToExpand is surely viewable (because this item is)
2341                 if (parentToExpand->initialExpandStatus() == Item::ExpandExecuted) {
2342                     break;
2343                 }
2344 
2345                 mView->expand(q->index(parentToExpand, 0));
2346 
2347                 parentToExpand->setInitialExpandStatus(Item::ExpandExecuted);
2348                 parentToExpand = parentToExpand->parent();
2349             }
2350         } else {
2351             // It isn't viewable or UI is disconnected: climb up marking only
2352             Item *parentToExpand = pParent->parent();
2353             while (parentToExpand) {
2354                 if (parentToExpand == mRootItem) {
2355                     break; // no need to set it expanded
2356                 }
2357                 parentToExpand->setInitialExpandStatus(Item::ExpandNeeded);
2358                 parentToExpand = parentToExpand->parent();
2359             }
2360         }
2361     }
2362 
2363     if (mi->isViewable()) {
2364         // mi is now viewable
2365 
2366         // sync subtree expanded status
2367         if (childNeedsExpanding) {
2368             if (mi->childItemCount() > 0) {
2369                 if (mModelForItemFunctions) { // the UI is not disconnected
2370                     syncExpandedStateOfSubtree(mi); // sync the real state in the view
2371                 }
2372             }
2373         }
2374 
2375         // apply the filter, if needed
2376         if (mFilter) {
2377             Q_ASSERT(mModelForItemFunctions); // the UI must be NOT disconnected here
2378 
2379             // apply the filter to subtree
2380             if (applyFilterToSubtree(mi, q->index(pParent, 0))) {
2381                 // mi matched, expand parents (unconditionally)
2382                 mView->ensureDisplayedWithParentsExpanded(mi);
2383             }
2384         }
2385     }
2386 
2387     // Now we need to propagate the property changes the upper levels.
2388 
2389     // If we have just inserted a message inside the root then no work needs to be done:
2390     // no grouping is in effect and the message is already in the right place.
2391     if (pParent == mRootItem) {
2392         return;
2393     }
2394 
2395     // If we have just removed the item from this parent and re-inserted it
2396     // then this operation was a simple re-sort. The code above didn't update
2397     // the properties when removing the item so we don't actually need
2398     // to make the updates back.
2399     if (oldParentWasTheSame) {
2400         return;
2401     }
2402 
2403     // FIXME: OPTIMIZE THIS: First propagate changes THEN syncExpandedStateOfSubtree()
2404     //        and applyFilterToSubtree... (needs some thinking though).
2405 
2406     // Time to propagate up.
2407     propagateItemPropertiesToParent(mi);
2408 
2409     // Aaah.. we're done. Time for a thea ? :)
2410 }
2411 
2412 // FIXME: ThreadItem ?
2413 //
2414 // Foo Bar, Joe Thommason, Martin Rox ... Eddie Maiden                    <date of the thread>
2415 // Title                                      <number of messages>, Last by xxx <inner status>
2416 //
2417 // When messages are added, mark it as dirty only (?)
2418 
viewItemJobStepInternalForJobPass5(ViewItemJob * job,QElapsedTimer elapsedTimer)2419 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass5(ViewItemJob *job, QElapsedTimer elapsedTimer)
2420 {
2421     // In this pass we scan the group headers that are in mGroupHeadersThatNeedUpdate.
2422     // Empty groups get deleted while the other ones are re-sorted.
2423 
2424     int curIndex = job->currentIndex();
2425 
2426     auto it = mGroupHeadersThatNeedUpdate.begin();
2427     auto end = mGroupHeadersThatNeedUpdate.end();
2428 
2429     while (it != end) {
2430         if ((*it)->childItemCount() == 0) {
2431             // group with no children, kill it
2432             (*it)->parent()->takeChildItem(mModelForItemFunctions, *it);
2433             mGroupHeaderItemHash.remove((*it)->label());
2434 
2435             // If we were going to restore its position after the job step, well.. we can't do it anymore.
2436             if (mCurrentItemToRestoreAfterViewItemJobStep == (*it)) {
2437                 mCurrentItemToRestoreAfterViewItemJobStep = nullptr;
2438             }
2439 
2440             // bye bye
2441             delete *it;
2442         } else {
2443             // Group with children: probably needs re-sorting.
2444 
2445             // Re-sorting here is an expensive operation.
2446             // In fact groups have been put in the QHash above on the assumption
2447             // that re-sorting *might* be needed but no real (expensive) check
2448             // has been done yet. Also by sorting a single group we might actually
2449             // put the others in the right place.
2450             // So finally check if re-sorting is *really* needed.
2451             bool needsReSorting;
2452 
2453             // A macro really improves readability here.
2454 #define CHECK_IF_GROUP_NEEDS_RESORTING(_ItemDateComparator)                                                                                                    \
2455     switch (mSortOrder->groupSortDirection()) {                                                                                                                \
2456     case SortOrder::Ascending:                                                                                                                                 \
2457         needsReSorting = (*it)->parent()->d_ptr->childItemNeedsReSorting<_ItemDateComparator, true>(*it);                                                      \
2458         break;                                                                                                                                                 \
2459     case SortOrder::Descending:                                                                                                                                \
2460         needsReSorting = (*it)->parent()->d_ptr->childItemNeedsReSorting<_ItemDateComparator, false>(*it);                                                     \
2461         break;                                                                                                                                                 \
2462     default: /* should never happen */                                                                                                                         \
2463         needsReSorting = false;                                                                                                                                \
2464         break;                                                                                                                                                 \
2465     }
2466 
2467             switch (mSortOrder->groupSorting()) {
2468             case SortOrder::SortGroupsByDateTime:
2469                 CHECK_IF_GROUP_NEEDS_RESORTING(ItemDateComparator)
2470                 break;
2471             case SortOrder::SortGroupsByDateTimeOfMostRecent:
2472                 CHECK_IF_GROUP_NEEDS_RESORTING(ItemMaxDateComparator)
2473                 break;
2474             case SortOrder::SortGroupsBySenderOrReceiver:
2475                 CHECK_IF_GROUP_NEEDS_RESORTING(ItemSenderOrReceiverComparator)
2476                 break;
2477             case SortOrder::SortGroupsBySender:
2478                 CHECK_IF_GROUP_NEEDS_RESORTING(ItemSenderComparator)
2479                 break;
2480             case SortOrder::SortGroupsByReceiver:
2481                 CHECK_IF_GROUP_NEEDS_RESORTING(ItemReceiverComparator)
2482                 break;
2483             case SortOrder::NoGroupSorting:
2484                 needsReSorting = false;
2485                 break;
2486             default:
2487                 // Should never happen... just assume re-sorting is not needed
2488                 needsReSorting = false;
2489                 break;
2490             }
2491 
2492             if (needsReSorting) {
2493                 attachGroup(*it); // it will first detach and then re-attach in the proper place
2494             }
2495         }
2496 
2497         it = mGroupHeadersThatNeedUpdate.erase(it);
2498 
2499         curIndex++;
2500 
2501         // FIXME: In fact a single update is likely to manipulate
2502         //        a subtree with a LOT of messages inside. If interactivity is favored
2503         //        we should check the time really more often.
2504         if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) {
2505             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
2506                 if (it != mGroupHeadersThatNeedUpdate.end()) {
2507                     job->setCurrentIndex(curIndex);
2508                     return ViewItemJobInterrupted;
2509                 }
2510             }
2511         }
2512     }
2513 
2514     return ViewItemJobCompleted;
2515 }
2516 
viewItemJobStepInternalForJobPass4(ViewItemJob * job,QElapsedTimer elapsedTimer)2517 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass4(ViewItemJob *job, QElapsedTimer elapsedTimer)
2518 {
2519     // In this pass we scan mUnassignedMessageListForPass4 which now
2520     // contains both items with parents and items without parents.
2521     // We scan mUnassignedMessageList for messages without parent (the ones that haven't been
2522     // attached to the viewable tree yet) and find a suitable group for them. Then we simply
2523     // clear mUnassignedMessageList.
2524 
2525     // We call this pass "Grouping"
2526 
2527     int curIndex = job->currentIndex();
2528     int endIndex = job->endIndex();
2529 
2530     while (curIndex <= endIndex) {
2531         MessageItem *mi = mUnassignedMessageListForPass4[curIndex];
2532         if (!mi->parent()) {
2533             // Unassigned item: thread leader, insert into the proper group.
2534             // Locate the group (or root if no grouping requested)
2535             attachMessageToGroupHeader(mi);
2536         } else {
2537             // A parent was already assigned in Pass3: we have nothing to do here
2538         }
2539         curIndex++;
2540 
2541         // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate
2542         //        a subtree with a LOT of messages inside. If interactivity is favored
2543         //        we should check the time really more often.
2544         if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) {
2545             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
2546                 if (curIndex <= endIndex) {
2547                     job->setCurrentIndex(curIndex);
2548                     return ViewItemJobInterrupted;
2549                 }
2550             }
2551         }
2552     }
2553 
2554     mUnassignedMessageListForPass4.clear();
2555     return ViewItemJobCompleted;
2556 }
2557 
viewItemJobStepInternalForJobPass3(ViewItemJob * job,QElapsedTimer elapsedTimer)2558 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass3(ViewItemJob *job, QElapsedTimer elapsedTimer)
2559 {
2560     // In this pass we scan the mUnassignedMessageListForPass3 and try to do construct the threads
2561     // by using subject based threading. If subject based threading is not in effect then
2562     // this pass turns to a nearly-no-op: at the end of Pass2 we have swapped the lists
2563     // and mUnassignedMessageListForPass3 is actually empty.
2564 
2565     // We don't shrink the mUnassignedMessageListForPass3 for two reasons:
2566     // - It would mess up this chunked algorithm by shifting indexes
2567     // - mUnassignedMessageList is a QList which is basically an array. It's faster
2568     //   to traverse an array of N entries than to remove K>0 entries one by one and
2569     //   to traverse the remaining N-K entries.
2570 
2571     int curIndex = job->currentIndex();
2572     int endIndex = job->endIndex();
2573 
2574     while (curIndex <= endIndex) {
2575         // If we're here, then threading is requested for sure.
2576         auto mi = mUnassignedMessageListForPass3[curIndex];
2577         if ((!mi->parent()) || (mi->threadingStatus() == MessageItem::ParentMissing)) {
2578             // Parent is missing (either "physically" with the item being not attached or "logically"
2579             // with the item being attached to a group or directly to the root.
2580             if (mi->subjectIsPrefixed()) {
2581                 // We can try to guess it
2582                 auto mparent = guessMessageParent(mi);
2583 
2584                 if (mparent) {
2585                     // imperfect parent found
2586                     if (mi->isViewable()) {
2587                         // mi was already viewable, we're just trying to re-parent it better...
2588                         attachMessageToParent(mparent, mi);
2589                         if (!mparent->isViewable()) {
2590                             // re-attach it immediately (so current item is not lost)
2591                             auto topmost = mparent->topmostMessage();
2592                             Q_ASSERT(!topmost->parent()); // groups are always viewable!
2593                             topmost->setThreadingStatus(MessageItem::ParentMissing);
2594                             attachMessageToGroupHeader(topmost);
2595                         }
2596                     } else {
2597                         // mi wasn't viewable yet.. no need to attach parent
2598                         attachMessageToParent(mparent, mi);
2599                     }
2600                     // and we're done for now
2601                 } else {
2602                     // so parent not found, (threadingStatus() is either MessageItem::ParentMissing or MessageItem::NonThreadable)
2603                     Q_ASSERT((mi->threadingStatus() == MessageItem::ParentMissing) || (mi->threadingStatus() == MessageItem::NonThreadable));
2604                     mUnassignedMessageListForPass4.append(mi); // this is ~O(1)
2605                     // and wait for Pass4
2606                 }
2607             } else {
2608                 // can't guess the parent as the subject isn't prefixed
2609                 Q_ASSERT((mi->threadingStatus() == MessageItem::ParentMissing) || (mi->threadingStatus() == MessageItem::NonThreadable));
2610                 mUnassignedMessageListForPass4.append(mi); // this is ~O(1)
2611                 // and wait for Pass4
2612             }
2613         } else {
2614             // Has a parent: either perfect parent already found or non threadable.
2615             // Since we don't end here if mi has status of parent missing then mi must not have imperfect parent.
2616             Q_ASSERT(mi->threadingStatus() != MessageItem::ImperfectParentFound);
2617             Q_ASSERT(mi->isViewable());
2618         }
2619 
2620         curIndex++;
2621 
2622         // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate
2623         //        a subtree with a LOT of messages inside. If interactivity is favored
2624         //        we should check the time really more often.
2625         if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) {
2626             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
2627                 if (curIndex <= endIndex) {
2628                     job->setCurrentIndex(curIndex);
2629                     return ViewItemJobInterrupted;
2630                 }
2631             }
2632         }
2633     }
2634 
2635     mUnassignedMessageListForPass3.clear();
2636     return ViewItemJobCompleted;
2637 }
2638 
viewItemJobStepInternalForJobPass2(ViewItemJob * job,QElapsedTimer elapsedTimer)2639 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass2(ViewItemJob *job, QElapsedTimer elapsedTimer)
2640 {
2641     // In this pass we scan the mUnassignedMessageList and try to do construct the threads.
2642     // If some thread leader message got attached to the viewable tree in Pass1Fill then
2643     // we'll also attach all of its children too. The thread leaders we were unable
2644     // to attach in Pass1Fill and their children (which we find here) will make it to the small Pass3
2645 
2646     // We don't shrink the mUnassignedMessageList for two reasons:
2647     // - It would mess up this chunked algorithm by shifting indexes
2648     // - mUnassignedMessageList is a QList which is basically an array. It's faster
2649     //   to traverse an array of N entries than to remove K>0 entries one by one and
2650     //   to traverse the remaining N-K entries.
2651 
2652     // We call this pass "Threading"
2653 
2654     int curIndex = job->currentIndex();
2655     int endIndex = job->endIndex();
2656 
2657     while (curIndex <= endIndex) {
2658         // If we're here, then threading is requested for sure.
2659         auto mi = mUnassignedMessageListForPass2[curIndex];
2660         // The item may or may not have a parent.
2661         // If it has no parent or it has a temporary one (mi->parent() && mi->threadingStatus() == MessageItem::ParentMissing)
2662         // then we attempt to (re-)thread it. Otherwise we just do nothing (the job has already been done by the previous steps).
2663         if ((!mi->parent()) || (mi->threadingStatus() == MessageItem::ParentMissing)) {
2664             qint64 parentId;
2665             auto mparent = mThreadingCache.parentForItem(mi, parentId);
2666             if (mparent && !mparent->hasAncestor(mi)) {
2667                 mi->setThreadingStatus(MessageItem::PerfectParentFound);
2668                 attachMessageToParent(mparent, mi, SkipCacheUpdate);
2669             } else {
2670                 if (parentId > 0) {
2671                     // In second pass we have all available Items in mThreadingCache already. If
2672                     // mThreadingCache.parentForItem() returns null, but returns valid parentId then
2673                     // the Item was removed from Akonadi and our threading cache is out-of-date.
2674                     mThreadingCache.expireParent(mi);
2675                     mparent = findMessageParent(mi);
2676                 } else if (parentId < 0) {
2677                     mparent = findMessageParent(mi);
2678                 } else {
2679                     // parentId = 0: this message is a thread leader so don't
2680                     // bother resolving parent, it will be moved directly to
2681                     // Pass4 in the code below
2682                 }
2683 
2684                 if (mparent) {
2685                     // parent found, either perfect or imperfect
2686                     if (mi->isViewable()) {
2687                         // mi was already viewable, we're just trying to re-parent it better...
2688                         attachMessageToParent(mparent, mi);
2689                         if (!mparent->isViewable()) {
2690                             // re-attach it immediately (so current item is not lost)
2691                             auto topmost = mparent->topmostMessage();
2692                             Q_ASSERT(!topmost->parent()); // groups are always viewable!
2693                             topmost->setThreadingStatus(MessageItem::ParentMissing);
2694                             attachMessageToGroupHeader(topmost);
2695                         }
2696                     } else {
2697                         // mi wasn't viewable yet.. no need to attach parent
2698                         attachMessageToParent(mparent, mi);
2699                     }
2700                     // and we're done for now
2701                 } else {
2702                     // so parent not found, (threadingStatus() is either MessageItem::ParentMissing or MessageItem::NonThreadable)
2703                     switch (mi->threadingStatus()) {
2704                     case MessageItem::ParentMissing:
2705                         if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) {
2706                             // parent missing but still can be found in Pass3
2707                             mUnassignedMessageListForPass3.append(mi); // this is ~O(1)
2708                         } else {
2709                             // We're not doing subject based threading: will never be threaded, go straight to Pass4
2710                             mUnassignedMessageListForPass4.append(mi); // this is ~O(1)
2711                         }
2712                         break;
2713                     case MessageItem::NonThreadable:
2714                         // will never be threaded, go straight to Pass4
2715                         mUnassignedMessageListForPass4.append(mi); // this is ~O(1)
2716                         break;
2717                     default:
2718                         // a bug for sure
2719                         qCWarning(MESSAGELIST_LOG) << "ERROR: Invalid message threading status returned by findMessageParent()!";
2720                         Q_ASSERT(false);
2721                         break;
2722                     }
2723                 }
2724             }
2725         } else {
2726             // Has a parent: either perfect parent already found or non threadable.
2727             // Since we don't end here if mi has status of parent missing then mi must not have imperfect parent.
2728             Q_ASSERT(mi->threadingStatus() != MessageItem::ImperfectParentFound);
2729             if (!mi->isViewable()) {
2730                 qCWarning(MESSAGELIST_LOG) << "Non viewable message " << mi << " subject " << mi->subject().toUtf8().data();
2731                 Q_ASSERT(mi->isViewable());
2732             }
2733         }
2734 
2735         curIndex++;
2736 
2737         // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate
2738         //        a subtree with a LOT of messages inside. If interactivity is favored
2739         //        we should check the time really more often.
2740         if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) {
2741             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
2742                 if (curIndex <= endIndex) {
2743                     job->setCurrentIndex(curIndex);
2744                     return ViewItemJobInterrupted;
2745                 }
2746             }
2747         }
2748     }
2749 
2750     mUnassignedMessageListForPass2.clear();
2751     return ViewItemJobCompleted;
2752 }
2753 
viewItemJobStepInternalForJobPass1Fill(ViewItemJob * job,QElapsedTimer elapsedTimer)2754 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Fill(ViewItemJob *job, QElapsedTimer elapsedTimer)
2755 {
2756     // In this pass we scan the a contiguous region of the underlying storage (that is
2757     // assumed to be FLAT) and create the corresponding MessageItem objects.
2758     // The deal is to show items to the user as soon as possible so in this pass we
2759     // *TRY* to attach them to the viewable tree (which is rooted on mRootItem).
2760     // Messages we're unable to attach for some reason (mainly due to threading) get appended
2761     // to mUnassignedMessageList and wait for Pass2.
2762 
2763     // We call this pass "Processing"
2764 
2765     // Should we use the receiver or the sender field for sorting ?
2766     bool bUseReceiver = mStorageModelContainsOutboundMessages;
2767 
2768     // The begin storage index of our work
2769     int curIndex = job->currentIndex();
2770     // The end storage index of our work.
2771     int endIndex = job->endIndex();
2772 
2773     unsigned long msgToSelect = mPreSelectionMode == PreSelectLastSelected ? mStorageModel->preSelectedMessage() : 0;
2774 
2775     MessageItem *mi = nullptr;
2776 
2777     while (curIndex <= endIndex) {
2778         // Create the message item with no parent: we'll set it later
2779         if (!mi) {
2780             mi = new MessageItem();
2781         } else {
2782             // a MessageItem discarded by a previous iteration: reuse it.
2783             Q_ASSERT(mi->parent() == nullptr);
2784         }
2785 
2786         if (!mStorageModel->initializeMessageItem(mi, curIndex, bUseReceiver)) {
2787             // ugh
2788             qCWarning(MESSAGELIST_LOG) << "Fill of the MessageItem at storage row index " << curIndex << " failed";
2789             curIndex++;
2790             continue;
2791         }
2792 
2793         // If we're supposed to pre-select a specific message, check if it's this one.
2794         if (msgToSelect != 0 && msgToSelect == mi->uniqueId()) {
2795             // Found, it's this one.
2796             // But actually it's not viewable (so not selectable). We must wait
2797             // until the end of the job to be 100% sure. So here we just translate
2798             // the unique id to a MessageItem pointer and wait.
2799             mLastSelectedMessageInFolder = mi;
2800             msgToSelect = 0; // already found, don't bother checking anymore
2801         }
2802 
2803         // Update the newest/oldest message, since we might be supposed to select those later
2804         if (mi->date() != static_cast<uint>(-1)) {
2805             if (!mOldestItem || mOldestItem->date() > mi->date()) {
2806                 mOldestItem = mi;
2807             }
2808             if (!mNewestItem || mNewestItem->date() < mi->date()) {
2809                 mNewestItem = mi;
2810             }
2811         }
2812 
2813         // Ok.. it passed the initial checks: we will not be discarding it.
2814         // Make this message item an invariant index to the underlying model storage.
2815         mInvariantRowMapper->createModelInvariantIndex(curIndex, mi);
2816 
2817         // Attempt to do threading as soon as possible (to display items to the user)
2818         if (mAggregation->threading() != Aggregation::NoThreading) {
2819             // Threading is requested
2820 
2821             // Fetch the data needed for proper threading
2822             // Add the item to the threading caches
2823 
2824             switch (mAggregation->threading()) {
2825             case Aggregation::PerfectReferencesAndSubject:
2826                 mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingReferencesAndSubject);
2827 
2828                 // We also need to build the subject/reference-based threading cache
2829                 addMessageToReferencesBasedThreadingCache(mi);
2830                 addMessageToSubjectBasedThreadingCache(mi);
2831                 break;
2832             case Aggregation::PerfectAndReferences:
2833                 mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingPlusReferences);
2834                 addMessageToReferencesBasedThreadingCache(mi);
2835                 break;
2836             default:
2837                 mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingOnly);
2838                 break;
2839             }
2840 
2841             // Perfect/References threading cache
2842             mThreadingCacheMessageIdMD5ToMessageItem.insert(mi->messageIdMD5(), mi);
2843 
2844             // Register the current item into the threading cache
2845             mThreadingCache.addItemToCache(mi);
2846 
2847             // First of all look into the persistent cache
2848             qint64 parentId;
2849             Item *pParent = mThreadingCache.parentForItem(mi, parentId);
2850             if (pParent) {
2851                 // We already have the parent MessageItem. Attach current message
2852                 // to it and mark it as perfect
2853                 mi->setThreadingStatus(MessageItem::PerfectParentFound);
2854                 attachMessageToParent(pParent, mi);
2855             } else if (parentId > 0) {
2856                 // We don't have the parent MessageItem yet, but we do know the
2857                 // parent: delay for pass 2 when we will have the parent MessageItem
2858                 // for sure.
2859                 mi->setThreadingStatus(MessageItem::ParentMissing);
2860                 mUnassignedMessageListForPass2.append(mi);
2861             } else if (parentId == 0) {
2862                 // Message is a thread leader, skip straight to Pass4
2863                 mi->setThreadingStatus(MessageItem::NonThreadable);
2864                 mUnassignedMessageListForPass4.append(mi);
2865             } else {
2866                 // Check if this item is a perfect parent for some imperfectly threaded
2867                 // message (that is actually attached to it, but not necessarily to the
2868                 // viewable root). If it is, then remove the imperfect child from its
2869                 // current parent rebuild the hierarchy on the fly.
2870                 bool needsImmediateReAttach = false;
2871 
2872                 if (!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.isEmpty()) { // unlikely
2873                     const auto lImperfectlyThreaded = mThreadingCacheMessageInReplyToIdMD5ToMessageItem.values(mi->messageIdMD5());
2874                     for (const auto it : lImperfectlyThreaded) {
2875                         Q_ASSERT(it->parent());
2876                         Q_ASSERT(it->parent() != mi);
2877 
2878                         if (!((it->threadingStatus() == MessageItem::ImperfectParentFound) || (it->threadingStatus() == MessageItem::ParentMissing))) {
2879                             qCritical() << "Got message " << it << " with threading status" << it->threadingStatus();
2880                             Q_ASSERT_X(false, "ModelPrivate::viewItemJobStepInternalForJobPass1Fill", "Wrong threading status");
2881                         }
2882 
2883                         // If the item was already attached to the view then
2884                         // re-attach it immediately. This will avoid a message
2885                         // being displayed for a short while in the view and then
2886                         // disappear until a perfect parent isn't found.
2887                         if (it->isViewable()) {
2888                             needsImmediateReAttach = true;
2889                         }
2890 
2891                         it->setThreadingStatus(MessageItem::PerfectParentFound);
2892                         attachMessageToParent(mi, it);
2893                     }
2894                 }
2895 
2896                 // FIXME: Might look by "References" too, here... (?)
2897 
2898                 // Attempt to do threading with anything we already have in caches until now
2899                 // Note that this is likely to work since thread-parent messages tend
2900                 // to come before thread-children messages in the folders (simply because of
2901                 // date of arrival).
2902 
2903                 // First of all try to find a "perfect parent", that is the message for that
2904                 // we have the ID in the "In-Reply-To" field. This is actually done by using
2905                 // MD5 caches of the message ids because of speed. Collisions are very unlikely.
2906 
2907                 const QByteArray md5 = mi->inReplyToIdMD5();
2908                 if (!md5.isEmpty()) {
2909                     // Have an In-Reply-To field MD5.
2910                     // In well behaved mailing lists 70% of the threadable messages get a parent here :)
2911                     pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr);
2912 
2913                     if (pParent) { // very likely
2914                         // Take care of self-referencing (which is always possible)
2915                         // and circular In-Reply-To reference loops which are possible
2916                         // in case this item was found to be a perfect parent for some
2917                         // imperfectly threaded message just above.
2918                         if ((mi == pParent) // self referencing message
2919                             || ((mi->childItemCount() > 0) // mi already has children, this is fast to determine
2920                                 && pParent->hasAncestor(mi) // pParent is in the mi's children tree
2921                                 )) {
2922                             // Bad, bad message.. it has In-Reply-To equal to Message-Id
2923                             // or it's in a circular In-Reply-To reference loop.
2924                             // Will wait for Pass2 with References-Id only
2925                             qCWarning(MESSAGELIST_LOG) << "Circular In-Reply-To reference loop detected in the message tree";
2926                             mUnassignedMessageListForPass2.append(mi);
2927                         } else {
2928                             // wow, got a perfect parent for this message!
2929                             mi->setThreadingStatus(MessageItem::PerfectParentFound);
2930                             attachMessageToParent(pParent, mi);
2931                             // we're done with this message (also for Pass2)
2932                         }
2933                     } else {
2934                         // got no parent
2935                         // will have to wait Pass2
2936                         mUnassignedMessageListForPass2.append(mi);
2937                     }
2938                 } else {
2939                     // No In-Reply-To header.
2940 
2941                     bool mightHaveOtherMeansForThreading;
2942 
2943                     switch (mAggregation->threading()) {
2944                     case Aggregation::PerfectReferencesAndSubject:
2945                         mightHaveOtherMeansForThreading = mi->subjectIsPrefixed() || !mi->referencesIdMD5().isEmpty();
2946                         break;
2947                     case Aggregation::PerfectAndReferences:
2948                         mightHaveOtherMeansForThreading = !mi->referencesIdMD5().isEmpty();
2949                         break;
2950                     case Aggregation::PerfectOnly:
2951                         mightHaveOtherMeansForThreading = false;
2952                         break;
2953                     default:
2954                         // BUG: there shouldn't be other values (NoThreading is excluded in an upper branch)
2955                         Q_ASSERT(false);
2956                         mightHaveOtherMeansForThreading = false; // make gcc happy
2957                         break;
2958                     }
2959 
2960                     if (mightHaveOtherMeansForThreading) {
2961                         // We might have other means for threading this message, wait until Pass2
2962                         mUnassignedMessageListForPass2.append(mi);
2963                     } else {
2964                         // No other means for threading this message. This is either
2965                         // a standalone message or a thread leader.
2966                         // If there is no grouping in effect or thread leaders are just the "topmost"
2967                         // messages then we might be done with this one.
2968                         if ((mAggregation->grouping() == Aggregation::NoGrouping) || (mAggregation->threadLeader() == Aggregation::TopmostMessage)) {
2969                             // We're done with this message: it will be surely either toplevel (no grouping in effect)
2970                             // or a thread leader with a well defined group. Do it :)
2971                             // qCDebug(MESSAGELIST_LOG) << "Setting message status from " << mi->threadingStatus() << " to non threadable (1) " << mi;
2972                             mi->setThreadingStatus(MessageItem::NonThreadable);
2973                             // Locate the parent group for this item
2974                             attachMessageToGroupHeader(mi);
2975                             // we're done with this message (also for Pass2)
2976                         } else {
2977                             // Threads belong to the most recent message in the thread. This means
2978                             // that we have to wait until Pass2 or Pass3 to assign a group.
2979                             mUnassignedMessageListForPass2.append(mi);
2980                         }
2981                     }
2982                 }
2983 
2984                 if (needsImmediateReAttach && !mi->isViewable()) {
2985                     // The item gathered previously viewable children. They must be immediately
2986                     // re-shown. So this item must currently be attached to the view.
2987                     // This is a temporary measure: it will be probably still moved.
2988                     MessageItem *topmost = mi->topmostMessage();
2989                     Q_ASSERT(topmost->threadingStatus() == MessageItem::ParentMissing);
2990                     attachMessageToGroupHeader(topmost);
2991                 }
2992             }
2993         } else {
2994             // else no threading requested: we don't even need Pass2
2995             // set not threadable status (even if it might be not true, but in this mode we don't care)
2996             // qCDebug(MESSAGELIST_LOG) << "Setting message status from " << mi->threadingStatus() << " to non threadable (2) " << mi;
2997             mi->setThreadingStatus(MessageItem::NonThreadable);
2998             // locate the parent group for this item
2999             if (mAggregation->grouping() == Aggregation::NoGrouping) {
3000                 attachMessageToParent(mRootItem, mi); // no groups requested, attach directly to root
3001             } else {
3002                 attachMessageToGroupHeader(mi);
3003             }
3004             // we're done with this message (also for Pass2)
3005         }
3006 
3007         mi = nullptr; // this item was pushed somewhere, create a new one at next iteration
3008         curIndex++;
3009 
3010         if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) {
3011             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3012                 if (curIndex <= endIndex) {
3013                     job->setCurrentIndex(curIndex);
3014                     return ViewItemJobInterrupted;
3015                 }
3016             }
3017         }
3018     }
3019 
3020     if (mi) {
3021         delete mi;
3022     }
3023     return ViewItemJobCompleted;
3024 }
3025 
viewItemJobStepInternalForJobPass1Cleanup(ViewItemJob * job,QElapsedTimer elapsedTimer)3026 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Cleanup(ViewItemJob *job, QElapsedTimer elapsedTimer)
3027 {
3028     Q_ASSERT(mModelForItemFunctions); // UI must be not disconnected here
3029     // In this pass we remove the MessageItem objects that are present in the job
3030     // and put their children in the unassigned message list.
3031 
3032     // Note that this list in fact contains MessageItem objects (we need dynamic_cast<>).
3033     QList<ModelInvariantIndex *> *invalidatedMessages = job->invariantIndexList();
3034 
3035     // We don't shrink the invalidatedMessages because it's basically an array.
3036     // It's faster to traverse an array of N entries than to remove K>0 entries
3037     // one by one and to traverse the remaining N-K entries.
3038 
3039     // The begin index of our work
3040     int curIndex = job->currentIndex();
3041     // The end index of our work.
3042     int endIndex = job->endIndex();
3043 
3044     if (curIndex == job->startIndex()) {
3045         Q_ASSERT(mOrphanChildrenHash.isEmpty());
3046     }
3047 
3048     while (curIndex <= endIndex) {
3049         // Get the underlying storage message data...
3050         auto dyingMessage = dynamic_cast<MessageItem *>(invalidatedMessages->at(curIndex));
3051         // This MUST NOT be null (otherwise we have a bug somewhere in this file).
3052         Q_ASSERT(dyingMessage);
3053 
3054         // If we were going to pre-select this message but we were interrupted
3055         // *before* it was actually made viewable, we just clear the pre-selection pointer
3056         // and unique id (abort pre-selection).
3057         if (dyingMessage == mLastSelectedMessageInFolder) {
3058             mLastSelectedMessageInFolder = nullptr;
3059             mPreSelectionMode = PreSelectNone;
3060         }
3061 
3062         // remove the message from any pending user job
3063         if (mPersistentSetManager) {
3064             mPersistentSetManager->removeMessageItemFromAllSets(dyingMessage);
3065             if (mPersistentSetManager->setCount() < 1) {
3066                 delete mPersistentSetManager;
3067                 mPersistentSetManager = nullptr;
3068             }
3069         }
3070 
3071         // Remove the message from threading cache before we start moving up the
3072         // children, so that they don't get mislead by the cache
3073         mThreadingCache.expireParent(dyingMessage);
3074 
3075         if (dyingMessage->parent()) {
3076             // Handle saving the current selection: if this item was the current before the step
3077             // then zero it out. We have killed it and it's OK for the current item to change.
3078 
3079             if (dyingMessage == mCurrentItemToRestoreAfterViewItemJobStep) {
3080                 Q_ASSERT(dyingMessage->isViewable());
3081                 // Try to select the item below the removed one as it helps in doing a "readon" of emails:
3082                 // you read a message, decide to delete it and then go to the next.
3083                 // Qt tends to select the message above the removed one instead (this is a hardcoded logic in
3084                 // QItemSelectionModelPrivate::_q_rowsAboutToBeRemoved()).
3085                 mCurrentItemToRestoreAfterViewItemJobStep = mView->messageItemAfter(dyingMessage, MessageTypeAny, false);
3086 
3087                 if (!mCurrentItemToRestoreAfterViewItemJobStep) {
3088                     // There is no item below. Try the item above.
3089                     // We still do it better than qt which tends to find the *thread* above
3090                     // instead of the item above.
3091                     mCurrentItemToRestoreAfterViewItemJobStep = mView->messageItemBefore(dyingMessage, MessageTypeAny, false);
3092                 }
3093 
3094                 Q_ASSERT((!mCurrentItemToRestoreAfterViewItemJobStep) || mCurrentItemToRestoreAfterViewItemJobStep->isViewable());
3095             }
3096 
3097             if (dyingMessage->isViewable() && ((dyingMessage)->childItemCount() > 0) // has children
3098                 && mView->isExpanded(q->index(dyingMessage, 0)) // is actually expanded
3099             ) {
3100                 saveExpandedStateOfSubtree(dyingMessage);
3101             }
3102 
3103             auto oldParent = dyingMessage->parent();
3104             oldParent->takeChildItem(q, dyingMessage);
3105 
3106             // FIXME: This can generate many message movements.. it would be nicer
3107             //        to start from messages that are higher in the hierarchy so
3108             //        we would need to move less stuff above.
3109 
3110             if (oldParent != mRootItem) {
3111                 messageDetachedUpdateParentProperties(oldParent, dyingMessage);
3112             }
3113 
3114             // We might have already removed its parent from the view, so it
3115             // might already be in the orphan child hash...
3116             if (dyingMessage->threadingStatus() == MessageItem::ParentMissing) {
3117                 mOrphanChildrenHash.remove(dyingMessage); // this can turn to a no-op (dyingMessage not present in fact)
3118             }
3119         } else {
3120             // The dying message had no parent: this should happen only if it's already an orphan
3121 
3122             Q_ASSERT(dyingMessage->threadingStatus() == MessageItem::ParentMissing);
3123             Q_ASSERT(mOrphanChildrenHash.contains(dyingMessage));
3124             Q_ASSERT(dyingMessage != mCurrentItemToRestoreAfterViewItemJobStep);
3125 
3126             mOrphanChildrenHash.remove(dyingMessage);
3127         }
3128 
3129         if (mAggregation->threading() != Aggregation::NoThreading) {
3130             // Threading is requested: remove the message from threading caches.
3131 
3132             // Remove from the cache of potential parent items
3133             mThreadingCacheMessageIdMD5ToMessageItem.remove(dyingMessage->messageIdMD5());
3134 
3135             // If we also have a cache for subject/reference-based threading then remove the message from there too
3136             if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) {
3137                 removeMessageFromReferencesBasedThreadingCache(dyingMessage);
3138                 removeMessageFromSubjectBasedThreadingCache(dyingMessage);
3139             } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) {
3140                 removeMessageFromReferencesBasedThreadingCache(dyingMessage);
3141             }
3142 
3143             // If this message wasn't perfectly parented then it might still be in another cache.
3144             switch (dyingMessage->threadingStatus()) {
3145             case MessageItem::ImperfectParentFound:
3146             case MessageItem::ParentMissing:
3147                 if (!dyingMessage->inReplyToIdMD5().isEmpty()) {
3148                     mThreadingCacheMessageInReplyToIdMD5ToMessageItem.remove(dyingMessage->inReplyToIdMD5());
3149                 }
3150                 break;
3151             default:
3152                 Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(dyingMessage->inReplyToIdMD5(), dyingMessage));
3153                 // make gcc happy
3154                 break;
3155             }
3156         }
3157 
3158         while (auto childItem = dyingMessage->firstChildItem()) {
3159             auto childMessage = dynamic_cast<MessageItem *>(childItem);
3160             Q_ASSERT(childMessage);
3161 
3162             dyingMessage->takeChildItem(q, childMessage);
3163 
3164             if (mAggregation->threading() != Aggregation::NoThreading) {
3165                 if (childMessage->threadingStatus() == MessageItem::PerfectParentFound) {
3166                     // If the child message was perfectly parented then now it had
3167                     // lost its perfect parent. Add to the cache of imperfectly parented.
3168                     if (!childMessage->inReplyToIdMD5().isEmpty()) {
3169                         Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(childMessage->inReplyToIdMD5(), childMessage));
3170                         mThreadingCacheMessageInReplyToIdMD5ToMessageItem.insert(childMessage->inReplyToIdMD5(), childMessage);
3171                     }
3172                 }
3173             }
3174 
3175             // Parent is gone
3176             childMessage->setThreadingStatus(MessageItem::ParentMissing);
3177 
3178             // If the child (or any message in its subtree) is going to be selected,
3179             // then we must immediately reattach it to a temporary group in order for the
3180             // selection to be preserved across multiple steps. Otherwise we could end
3181             // with the child-to-be-selected being non viewable at the end
3182             // of the view job step. Attach to a temporary group.
3183             if (
3184                 // child is going to be re-selected
3185                 (childMessage == mCurrentItemToRestoreAfterViewItemJobStep)
3186                 || (
3187                     // there is a message that is going to be re-selected
3188                     mCurrentItemToRestoreAfterViewItemJobStep && // that message is in the childMessage subtree
3189                     mCurrentItemToRestoreAfterViewItemJobStep->hasAncestor(childMessage))) {
3190                 attachMessageToGroupHeader(childMessage);
3191 
3192                 Q_ASSERT(childMessage->isViewable());
3193             }
3194 
3195             mOrphanChildrenHash.insert(childMessage, childMessage);
3196         }
3197 
3198         if (mNewestItem == dyingMessage) {
3199             mNewestItem = nullptr;
3200         }
3201         if (mOldestItem == dyingMessage) {
3202             mOldestItem = nullptr;
3203         }
3204 
3205         delete dyingMessage;
3206 
3207         curIndex++;
3208 
3209         // FIXME: Maybe we should check smaller steps here since the
3210         //        code above can generate large message tree movements
3211         //        for each single item we sweep in the invalidatedMessages list.
3212         if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) {
3213             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3214                 if (curIndex <= endIndex) {
3215                     job->setCurrentIndex(curIndex);
3216                     return ViewItemJobInterrupted;
3217                 }
3218             }
3219         }
3220     }
3221 
3222     // We looped over the entire deleted message list.
3223 
3224     job->setCurrentIndex(endIndex + 1);
3225 
3226     // A quick last cleaning pass: this is usually very fast so we don't have a real
3227     // Pass enumeration for it. We just include it as trailer of Pass1Cleanup to be executed
3228     // when job->currentIndex() > job->endIndex();
3229 
3230     // We move all the messages from the orphan child hash to the unassigned message
3231     // list and get them ready for the standard Pass2.
3232 
3233     auto it = mOrphanChildrenHash.begin();
3234     auto end = mOrphanChildrenHash.end();
3235 
3236     curIndex = 0;
3237 
3238     while (it != end) {
3239         mUnassignedMessageListForPass2.append(*it);
3240 
3241         it = mOrphanChildrenHash.erase(it);
3242 
3243         // This is still interruptible
3244 
3245         curIndex++;
3246 
3247         // FIXME: We could take "larger" steps here
3248         if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) {
3249             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3250                 if (it != mOrphanChildrenHash.end()) {
3251                     return ViewItemJobInterrupted;
3252                 }
3253             }
3254         }
3255     }
3256 
3257     return ViewItemJobCompleted;
3258 }
3259 
viewItemJobStepInternalForJobPass1Update(ViewItemJob * job,QElapsedTimer elapsedTimer)3260 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Update(ViewItemJob *job, QElapsedTimer elapsedTimer)
3261 {
3262     Q_ASSERT(mModelForItemFunctions); // UI must be not disconnected here
3263 
3264     // In this pass we simply update the MessageItem objects that are present in the job.
3265 
3266     // Note that this list in fact contains MessageItem objects (we need dynamic_cast<>).
3267     auto messagesThatNeedUpdate = job->invariantIndexList();
3268 
3269     // We don't shrink the messagesThatNeedUpdate because it's basically an array.
3270     // It's faster to traverse an array of N entries than to remove K>0 entries
3271     // one by one and to traverse the remaining N-K entries.
3272 
3273     // The begin index of our work
3274     int curIndex = job->currentIndex();
3275     // The end index of our work.
3276     int endIndex = job->endIndex();
3277 
3278     while (curIndex <= endIndex) {
3279         // Get the underlying storage message data...
3280         auto message = dynamic_cast<MessageItem *>(messagesThatNeedUpdate->at(curIndex));
3281         // This MUST NOT be null (otherwise we have a bug somewhere in this file).
3282         Q_ASSERT(message);
3283 
3284         int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(message);
3285 
3286         if (row < 0) {
3287             // Must have been invalidated (so it's basically about to be deleted)
3288             Q_ASSERT(!message->isValid());
3289             // Skip it here.
3290             curIndex++;
3291             continue;
3292         }
3293 
3294         time_t prevDate = message->date();
3295         time_t prevMaxDate = message->maxDate();
3296         bool toDoStatus = message->status().isToAct();
3297         bool prevUnreadStatus = !message->status().isRead();
3298         bool prevImportantStatus = message->status().isImportant();
3299 
3300         // The subject/reference based threading cache is sorted by date: we must remove
3301         // the item and re-insert it since updateMessageItemData() may change the date too.
3302         if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) {
3303             removeMessageFromReferencesBasedThreadingCache(message);
3304             removeMessageFromSubjectBasedThreadingCache(message);
3305         } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) {
3306             removeMessageFromReferencesBasedThreadingCache(message);
3307         }
3308 
3309         // Do update
3310         mStorageModel->updateMessageItemData(message, row);
3311         QModelIndex idx = q->index(message, 0);
3312         Q_EMIT q->dataChanged(idx, idx);
3313 
3314         // Reinsert the item to the cache, if needed
3315         if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) {
3316             addMessageToReferencesBasedThreadingCache(message);
3317             addMessageToSubjectBasedThreadingCache(message);
3318         } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) {
3319             addMessageToReferencesBasedThreadingCache(message);
3320         }
3321 
3322         int propertyChangeMask = 0;
3323 
3324         if (prevDate != message->date()) {
3325             propertyChangeMask |= DateChanged;
3326         }
3327         if (prevMaxDate != message->maxDate()) {
3328             propertyChangeMask |= MaxDateChanged;
3329         }
3330         if (toDoStatus != message->status().isToAct()) {
3331             propertyChangeMask |= ActionItemStatusChanged;
3332         }
3333         if (prevUnreadStatus != (!message->status().isRead())) {
3334             propertyChangeMask |= UnreadStatusChanged;
3335         }
3336         if (prevImportantStatus != (!message->status().isImportant())) {
3337             propertyChangeMask |= ImportantStatusChanged;
3338         }
3339 
3340         if (propertyChangeMask) {
3341             // Some message data has changed
3342             // now we need to handle the changes that might cause re-grouping/re-sorting
3343             // and propagate them to the parents.
3344 
3345             Item *pParent = message->parent();
3346 
3347             if (pParent && (pParent != mRootItem)) {
3348                 // The following function will return true if itemParent may be affected by the change.
3349                 // If the itemParent isn't affected, we stop climbing.
3350                 if (handleItemPropertyChanges(propertyChangeMask, pParent, message)) {
3351                     Q_ASSERT(message->parent()); // handleItemPropertyChanges() must never leave an item detached
3352 
3353                     // Note that actually message->parent() may be different than pParent since
3354                     // handleItemPropertyChanges() may have re-grouped it.
3355 
3356                     // Time to propagate up.
3357                     propagateItemPropertiesToParent(message);
3358                 }
3359             } // else there is no parent so the item isn't attached to the view: re-grouping/re-sorting not needed.
3360         } // else message data didn't change an there is nothing interesting to do
3361 
3362         // (re-)apply the filter, if needed
3363         if (mFilter && message->isViewable()) {
3364             // In all the other cases we (re-)apply the filter to the topmost subtree that this message is in.
3365             Item *pTopMostNonRoot = message->topmostNonRoot();
3366 
3367             Q_ASSERT(pTopMostNonRoot);
3368             Q_ASSERT(pTopMostNonRoot != mRootItem);
3369             Q_ASSERT(pTopMostNonRoot->parent() == mRootItem);
3370 
3371             // FIXME: The call below works, but it's expensive when we are updating
3372             //        a lot of items with filtering enabled. This is because the updated
3373             //        items are likely to be in the same subtree which we then filter multiple times.
3374             //        A point for us is that when filtering there shouldn't be really many
3375             //        items in the view so the user isn't going to update a lot of them at once...
3376             //        Well... anyway, the alternative would be to write yet another
3377             //        specialized routine that would update only the "message" item
3378             //        above and climb up eventually hiding parents (without descending the sibling subtrees again).
3379             //        If people complain about performance in this particular case I'll consider that solution.
3380 
3381             applyFilterToSubtree(pTopMostNonRoot, QModelIndex());
3382         } // otherwise there is no filter or the item isn't viewable: very likely
3383           // left detached while propagating property changes. Will filter it
3384           // on reattach.
3385 
3386         // Done updating this message
3387 
3388         curIndex++;
3389 
3390         // FIXME: Maybe we should check smaller steps here since the
3391         //        code above can generate large message tree movements
3392         //        for each single item we sweep in the messagesThatNeedUpdate list.
3393         if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) {
3394             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3395                 if (curIndex <= endIndex) {
3396                     job->setCurrentIndex(curIndex);
3397                     return ViewItemJobInterrupted;
3398                 }
3399             }
3400         }
3401     }
3402 
3403     return ViewItemJobCompleted;
3404 }
3405 
viewItemJobStepInternalForJob(ViewItemJob * job,QElapsedTimer elapsedTimer)3406 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJob(ViewItemJob *job, QElapsedTimer elapsedTimer)
3407 {
3408     // This function does a timed chunk of work for a single Fill View job.
3409     // It attempts to process messages until a timeout forces it to return to the caller.
3410 
3411     // A macro would improve readability here but since this is a good point
3412     // to place debugger breakpoints then we need it explicitly.
3413     // A (template) helper would need to pass many parameters and would not be inlined...
3414 
3415     if (job->currentPass() == ViewItemJob::Pass1Fill) {
3416         // We're in Pass1Fill of the job.
3417         switch (viewItemJobStepInternalForJobPass1Fill(job, elapsedTimer)) {
3418         case ViewItemJobInterrupted:
3419             // current job interrupted by timeout: propagate status to caller
3420             return ViewItemJobInterrupted;
3421             break;
3422         case ViewItemJobCompleted:
3423             // pass 1 has been completed
3424             // # TODO: Refactor this, make it virtual or whatever, but switch == bad, code duplication etc
3425             job->setCurrentPass(ViewItemJob::Pass2);
3426             job->setStartIndex(0);
3427             job->setEndIndex(mUnassignedMessageListForPass2.count() - 1);
3428             // take care of small jobs which never timeout by themselves because
3429             // of a small number of messages. At the end of each job check
3430             // the time used and if we're timeoutting and there is another job
3431             // then interrupt.
3432             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3433                 return ViewItemJobInterrupted;
3434             } // else proceed with the next pass
3435             break;
3436         default:
3437             // This is *really* a BUG
3438             qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result";
3439             Q_ASSERT(false);
3440             break;
3441         }
3442     } else if (job->currentPass() == ViewItemJob::Pass1Cleanup) {
3443         // We're in Pass1Cleanup of the job.
3444         switch (viewItemJobStepInternalForJobPass1Cleanup(job, elapsedTimer)) {
3445         case ViewItemJobInterrupted:
3446             // current job interrupted by timeout: propagate status to caller
3447             return ViewItemJobInterrupted;
3448             break;
3449         case ViewItemJobCompleted:
3450             // pass 1 has been completed
3451             job->setCurrentPass(ViewItemJob::Pass2);
3452             job->setStartIndex(0);
3453             job->setEndIndex(mUnassignedMessageListForPass2.count() - 1);
3454             // take care of small jobs which never timeout by themselves because
3455             // of a small number of messages. At the end of each job check
3456             // the time used and if we're timeoutting and there is another job
3457             // then interrupt.
3458             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3459                 return ViewItemJobInterrupted;
3460             } // else proceed with the next pass
3461             break;
3462         default:
3463             // This is *really* a BUG
3464             qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result";
3465             Q_ASSERT(false);
3466             break;
3467         }
3468     } else if (job->currentPass() == ViewItemJob::Pass1Update) {
3469         // We're in Pass1Update of the job.
3470         switch (viewItemJobStepInternalForJobPass1Update(job, elapsedTimer)) {
3471         case ViewItemJobInterrupted:
3472             // current job interrupted by timeout: propagate status to caller
3473             return ViewItemJobInterrupted;
3474             break;
3475         case ViewItemJobCompleted:
3476             // pass 1 has been completed
3477             // Since Pass2, Pass3 and Pass4 are empty for an Update operation
3478             // we simply skip them. (TODO: Triple-verify this assertion...).
3479             job->setCurrentPass(ViewItemJob::Pass5);
3480             job->setStartIndex(0);
3481             job->setEndIndex(mGroupHeadersThatNeedUpdate.count() - 1);
3482             // take care of small jobs which never timeout by themselves because
3483             // of a small number of messages. At the end of each job check
3484             // the time used and if we're timeoutting and there is another job
3485             // then interrupt.
3486             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3487                 return ViewItemJobInterrupted;
3488             } // else proceed with the next pass
3489             break;
3490         default:
3491             // This is *really* a BUG
3492             qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result";
3493             Q_ASSERT(false);
3494             break;
3495         }
3496     }
3497 
3498     // Pass1Fill/Pass1Cleanup/Pass1Update has been already completed.
3499 
3500     if (job->currentPass() == ViewItemJob::Pass2) {
3501         // We're in Pass2 of the job.
3502         switch (viewItemJobStepInternalForJobPass2(job, elapsedTimer)) {
3503         case ViewItemJobInterrupted:
3504             // current job interrupted by timeout: propagate status to caller
3505             return ViewItemJobInterrupted;
3506             break;
3507         case ViewItemJobCompleted:
3508             // pass 2 has been completed
3509             job->setCurrentPass(ViewItemJob::Pass3);
3510             job->setStartIndex(0);
3511             job->setEndIndex(mUnassignedMessageListForPass3.count() - 1);
3512             // take care of small jobs which never timeout by themselves because
3513             // of a small number of messages. At the end of each job check
3514             // the time used and if we're timeoutting and there is another job
3515             // then interrupt.
3516             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3517                 return ViewItemJobInterrupted;
3518             }
3519             // else proceed with the next pass
3520             break;
3521         default:
3522             // This is *really* a BUG
3523             qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result";
3524             Q_ASSERT(false);
3525             break;
3526         }
3527     }
3528 
3529     if (job->currentPass() == ViewItemJob::Pass3) {
3530         // We're in Pass3 of the job.
3531         switch (viewItemJobStepInternalForJobPass3(job, elapsedTimer)) {
3532         case ViewItemJobInterrupted:
3533             // current job interrupted by timeout: propagate status to caller
3534             return ViewItemJobInterrupted;
3535         case ViewItemJobCompleted:
3536             // pass 3 has been completed
3537             job->setCurrentPass(ViewItemJob::Pass4);
3538             job->setStartIndex(0);
3539             job->setEndIndex(mUnassignedMessageListForPass4.count() - 1);
3540             // take care of small jobs which never timeout by themselves because
3541             // of a small number of messages. At the end of each job check
3542             // the time used and if we're timeoutting and there is another job
3543             // then interrupt.
3544             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3545                 return ViewItemJobInterrupted;
3546             }
3547             // else proceed with the next pass
3548             break;
3549         default:
3550             // This is *really* a BUG
3551             qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result";
3552             Q_ASSERT(false);
3553             break;
3554         }
3555     }
3556 
3557     if (job->currentPass() == ViewItemJob::Pass4) {
3558         // We're in Pass4 of the job.
3559         switch (viewItemJobStepInternalForJobPass4(job, elapsedTimer)) {
3560         case ViewItemJobInterrupted:
3561             // current job interrupted by timeout: propagate status to caller
3562             return ViewItemJobInterrupted;
3563         case ViewItemJobCompleted:
3564             // pass 4 has been completed
3565             job->setCurrentPass(ViewItemJob::Pass5);
3566             job->setStartIndex(0);
3567             job->setEndIndex(mGroupHeadersThatNeedUpdate.count() - 1);
3568             // take care of small jobs which never timeout by themselves because
3569             // of a small number of messages. At the end of each job check
3570             // the time used and if we're timeoutting and there is another job
3571             // then interrupt.
3572             if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) {
3573                 return ViewItemJobInterrupted;
3574             }
3575             // else proceed with the next pass
3576             break;
3577         default:
3578             // This is *really* a BUG
3579             qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result";
3580             Q_ASSERT(false);
3581             break;
3582         }
3583     }
3584 
3585     // Pass4 has been already completed. Proceed to Pass5.
3586     return viewItemJobStepInternalForJobPass5(job, elapsedTimer);
3587 }
3588 
3589 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3590 
3591 // Namespace to collect all the vars and functions for KDEPIM_FOLDEROPEN_PROFILE
3592 namespace Stats
3593 {
3594 // Number of existing jobs/passes
3595 static const int numberOfPasses = ViewItemJob::LastIndex;
3596 
3597 // The pass in the last call of viewItemJobStepInternal(), used to detect when
3598 // a new pass starts
3599 static int lastPass = -1;
3600 
3601 // Total number of messages in the folder
3602 static int totalMessages;
3603 
3604 // Per-Job data
3605 static int numElements[numberOfPasses];
3606 static int totalTime[numberOfPasses];
3607 static int chunks[numberOfPasses];
3608 
3609 // Time, in msecs for some special operations
3610 static int expandingTreeTime;
3611 static int layoutChangeTime;
3612 
3613 // Descriptions of the job, for nicer debug output
3614 static const char *jobDescription[numberOfPasses] = {"Creating items from messages and simple threading",
3615                                                      "Removing messages",
3616                                                      "Updating messages",
3617                                                      "Additional Threading",
3618                                                      "Subject-Based threading",
3619                                                      "Grouping",
3620                                                      "Group resorting + cleanup"};
3621 
3622 // Timer to track time between start of first job and end of last job
3623 static QTime firstStartTime;
3624 
3625 // Timer to track time the current job takes
3626 static QTime currentJobStartTime;
3627 
3628 // Zeros the stats, to be called when the first job starts
resetStats()3629 static void resetStats()
3630 {
3631     totalMessages = 0;
3632     layoutChangeTime = 0;
3633     expandingTreeTime = 0;
3634     lastPass = -1;
3635     for (int i = 0; i < numberOfPasses; ++i) {
3636         numElements[i] = 0;
3637         totalTime[i] = 0;
3638         chunks[i] = 0;
3639     }
3640 }
3641 } // namespace Stats
3642 
printStatistics()3643 void ModelPrivate::printStatistics()
3644 {
3645     using namespace Stats;
3646     int totalTotalTime = 0;
3647     int completeTime = firstStartTime.elapsed();
3648     for (int i = 0; i < numberOfPasses; ++i) {
3649         totalTotalTime += totalTime[i];
3650     }
3651 
3652     float msgPerSecond = totalMessages / (totalTotalTime / 1000.0f);
3653     float msgPerSecondComplete = totalMessages / (completeTime / 1000.0f);
3654 
3655     int messagesWithSameSubjectAvg = 0;
3656     int messagesWithSameSubjectMax = 0;
3657     for (const auto messages : std::as_const(mThreadingCacheMessageSubjectMD5ToMessageItem)) {
3658         if (messages->size() > messagesWithSameSubjectMax) {
3659             messagesWithSameSubjectMax = messages->size();
3660         }
3661         messagesWithSameSubjectAvg += messages->size();
3662     }
3663     messagesWithSameSubjectAvg = messagesWithSameSubjectAvg / (float)mThreadingCacheMessageSubjectMD5ToMessageItem.size();
3664 
3665     int totalThreads = 0;
3666     if (!mGroupHeaderItemHash.isEmpty()) {
3667         for (const GroupHeaderItem *groupHeader : std::as_const(mGroupHeaderItemHash)) {
3668             totalThreads += groupHeader->childItemCount();
3669         }
3670     } else {
3671         totalThreads = mRootItem->childItemCount();
3672     }
3673 
3674     qCDebug(MESSAGELIST_LOG) << "Finished filling the view with" << totalMessages << "messages";
3675     qCDebug(MESSAGELIST_LOG) << "That took" << totalTotalTime << "msecs inside the model and" << completeTime << "in total.";
3676     qCDebug(MESSAGELIST_LOG) << (totalTotalTime / (float)completeTime) * 100.0f << "percent of the time was spent in the model.";
3677     qCDebug(MESSAGELIST_LOG) << "Time for layoutChanged(), in msecs:" << layoutChangeTime << "(" << (layoutChangeTime / (float)totalTotalTime) * 100.0f
3678                              << "percent )";
3679     qCDebug(MESSAGELIST_LOG) << "Time to expand tree, in msecs:" << expandingTreeTime << "(" << (expandingTreeTime / (float)totalTotalTime) * 100.0f
3680                              << "percent )";
3681     qCDebug(MESSAGELIST_LOG) << "Number of messages per second in the model:" << msgPerSecond;
3682     qCDebug(MESSAGELIST_LOG) << "Number of messages per second in total:" << msgPerSecondComplete;
3683     qCDebug(MESSAGELIST_LOG) << "Number of threads:" << totalThreads;
3684     qCDebug(MESSAGELIST_LOG) << "Number of groups:" << mGroupHeaderItemHash.size();
3685     qCDebug(MESSAGELIST_LOG) << "Messages per thread:" << totalMessages / (float)totalThreads;
3686     qCDebug(MESSAGELIST_LOG) << "Threads per group:" << totalThreads / (float)mGroupHeaderItemHash.size();
3687     qCDebug(MESSAGELIST_LOG) << "Messages with the same subject:"
3688                              << "Max:" << messagesWithSameSubjectMax << "Avg:" << messagesWithSameSubjectAvg;
3689     qCDebug(MESSAGELIST_LOG);
3690     qCDebug(MESSAGELIST_LOG) << "Now follows a breakdown of the jobs.";
3691     qCDebug(MESSAGELIST_LOG);
3692     for (int i = 0; i < numberOfPasses; ++i) {
3693         if (totalTime[i] == 0) {
3694             continue;
3695         }
3696         float elementsPerSecond = numElements[i] / (totalTime[i] / 1000.0f);
3697         float percent = totalTime[i] / (float)totalTotalTime * 100.0f;
3698         qCDebug(MESSAGELIST_LOG) << "----------------------------------------------";
3699         qCDebug(MESSAGELIST_LOG) << "Job" << i + 1 << "(" << jobDescription[i] << ")";
3700         qCDebug(MESSAGELIST_LOG) << "Share of complete time:" << percent << "percent";
3701         qCDebug(MESSAGELIST_LOG) << "Time in msecs:" << totalTime[i];
3702         qCDebug(MESSAGELIST_LOG) << "Number of elements:" << numElements[i]; // TODO: map of element string
3703         qCDebug(MESSAGELIST_LOG) << "Elements per second:" << elementsPerSecond;
3704         qCDebug(MESSAGELIST_LOG) << "Number of chunks:" << chunks[i];
3705         qCDebug(MESSAGELIST_LOG);
3706     }
3707 
3708     qCDebug(MESSAGELIST_LOG) << "==========================================================";
3709     resetStats();
3710 }
3711 
3712 #endif
3713 
viewItemJobStepInternal()3714 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternal()
3715 {
3716     // This function does a timed chunk of work in our View Fill operation.
3717     // It attempts to do processing until it either runs out of jobs
3718     // to be done or a timeout forces it to interrupt and jump back to the caller.
3719 
3720     QElapsedTimer elapsedTimer;
3721     elapsedTimer.start();
3722 
3723     while (!mViewItemJobs.isEmpty()) {
3724         // Have a job to do.
3725         ViewItemJob *job = mViewItemJobs.constFirst();
3726 
3727 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3728 
3729         // Here we check if an old job has just completed or if we are at the start of the
3730         // first job. We then initialize job data stuff and timers based on this.
3731 
3732         const int currentPass = job->currentPass();
3733         const bool firstChunk = currentPass != Stats::lastPass;
3734         if (currentPass != Stats::lastPass && Stats::lastPass != -1) {
3735             Stats::totalTime[Stats::lastPass] = Stats::currentJobStartTime.elapsed();
3736         }
3737         const bool firstJob = job->currentPass() == ViewItemJob::Pass1Fill && firstChunk;
3738         const int elements = job->endIndex() - job->startIndex();
3739         if (firstJob) {
3740             Stats::resetStats();
3741             Stats::totalMessages = elements;
3742             Stats::firstStartTime.restart();
3743         }
3744         if (firstChunk) {
3745             Stats::numElements[currentPass] = elements;
3746             Stats::currentJobStartTime.restart();
3747         }
3748         Stats::chunks[currentPass]++;
3749         Stats::lastPass = currentPass;
3750 
3751 #endif
3752 
3753         mViewItemJobStepIdleInterval = job->idleInterval();
3754         mViewItemJobStepChunkTimeout = job->chunkTimeout();
3755         mViewItemJobStepMessageCheckCount = job->messageCheckCount();
3756 
3757         if (job->disconnectUI()) {
3758             mModelForItemFunctions = nullptr; // disconnect the UI for this job
3759             Q_ASSERT(mLoading); // this must be true in the first job
3760             // FIXME: Should assert yet more that this is the very first job for this StorageModel
3761             //        Asserting only mLoading is not enough as we could be using a two-jobs loading strategy
3762             //        or this could be a job enqueued before the first job has completed.
3763         } else {
3764             // With a connected UI we need to avoid the view to update the scrollbars at EVERY insertion or expansion.
3765             // QTreeViewPrivate::updateScrollBars() is very expensive as it loops through ALL the items in the view every time.
3766             // We can't disable the function directly as it's hidden in the private data object of QTreeView
3767             // but we can disable the parent QTreeView::updateGeometries() instead.
3768             // We will trigger it "manually" at the end of the step.
3769             mView->ignoreUpdateGeometries(true);
3770 
3771             // Ok.. I know that this seems unbelieveable but disabling updates actually
3772             // causes a (significant) performance loss in most cases. This is probably because QTreeView
3773             // uses delayed layouts when updates are disabled which should be delayed but in
3774             // fact are "forced" by next item insertions. The delayed layout algorithm, then
3775             // is probably slower than the non-delayed one.
3776             // Disabling the paintEvent() doesn't seem to work either.
3777             // mView->setUpdatesEnabled( false );
3778         }
3779 
3780         switch (viewItemJobStepInternalForJob(job, elapsedTimer)) {
3781         case ViewItemJobInterrupted:
3782             // current job interrupted by timeout: will propagate status to caller
3783             // but before this, give some feedback to the user
3784 
3785             // FIXME: This is now inaccurate, think of something else
3786             switch (job->currentPass()) {
3787             case ViewItemJob::Pass1Fill:
3788             case ViewItemJob::Pass1Cleanup:
3789             case ViewItemJob::Pass1Update:
3790                 Q_EMIT q->statusMessage(i18np("Processed 1 Message of %2",
3791                                               "Processed %1 Messages of %2",
3792                                               job->currentIndex() - job->startIndex(),
3793                                               job->endIndex() - job->startIndex() + 1));
3794                 break;
3795             case ViewItemJob::Pass2:
3796                 Q_EMIT q->statusMessage(i18np("Threaded 1 Message of %2",
3797                                               "Threaded %1 Messages of %2",
3798                                               job->currentIndex() - job->startIndex(),
3799                                               job->endIndex() - job->startIndex() + 1));
3800                 break;
3801             case ViewItemJob::Pass3:
3802                 Q_EMIT q->statusMessage(i18np("Threaded 1 Message of %2",
3803                                               "Threaded %1 Messages of %2",
3804                                               job->currentIndex() - job->startIndex(),
3805                                               job->endIndex() - job->startIndex() + 1));
3806                 break;
3807             case ViewItemJob::Pass4:
3808                 Q_EMIT q->statusMessage(i18np("Grouped 1 Thread of %2",
3809                                               "Grouped %1 Threads of %2",
3810                                               job->currentIndex() - job->startIndex(),
3811                                               job->endIndex() - job->startIndex() + 1));
3812                 break;
3813             case ViewItemJob::Pass5:
3814                 Q_EMIT q->statusMessage(i18np("Updated 1 Group of %2",
3815                                               "Updated %1 Groups of %2",
3816                                               job->currentIndex() - job->startIndex(),
3817                                               job->endIndex() - job->startIndex() + 1));
3818                 break;
3819             default:
3820                 break;
3821             }
3822 
3823             if (!job->disconnectUI()) {
3824                 mView->ignoreUpdateGeometries(false);
3825                 // explicit call to updateGeometries() here
3826                 mView->updateGeometries();
3827             }
3828 
3829             return ViewItemJobInterrupted;
3830             break;
3831         case ViewItemJobCompleted:
3832 
3833             // If this job worked with a disconnected UI, Q_EMIT layoutChanged()
3834             // to reconnect it. We go back to normal operation now.
3835             if (job->disconnectUI()) {
3836                 mModelForItemFunctions = q;
3837                 // This call would destroy the expanded state of items.
3838                 // This is why when mModelForItemFunctions was 0 we didn't actually expand them
3839                 // but we just set a "ExpandNeeded" mark...
3840 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3841                 QTime layoutChangedTimer;
3842                 layoutChangedTimer.start();
3843 #endif
3844                 mView->modelAboutToEmitLayoutChanged();
3845                 Q_EMIT q->layoutChanged();
3846                 mView->modelEmittedLayoutChanged();
3847 
3848 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3849                 Stats::layoutChangeTime = layoutChangedTimer.elapsed();
3850                 QTime expandingTime;
3851                 expandingTime.start();
3852 #endif
3853 
3854                 // expand all the items that need it in a single sweep
3855 
3856                 // FIXME: This takes quite a lot of time, it could be made an interruptible job
3857 
3858                 auto rootChildItems = mRootItem->childItems();
3859                 if (rootChildItems) {
3860                     for (const auto it : std::as_const(*rootChildItems)) {
3861                         if (it->initialExpandStatus() == Item::ExpandNeeded) {
3862                             syncExpandedStateOfSubtree(it);
3863                         }
3864                     }
3865                 }
3866 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3867                 Stats::expandingTreeTime = expandingTime.elapsed();
3868 #endif
3869             } else {
3870                 mView->ignoreUpdateGeometries(false);
3871                 // explicit call to updateGeometries() here
3872                 mView->updateGeometries();
3873             }
3874 
3875             // this job has been completed
3876             delete mViewItemJobs.takeFirst();
3877 
3878 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3879             // Last job finished!
3880             Stats::totalTime[currentPass] = Stats::currentJobStartTime.elapsed();
3881             printStatistics();
3882 #endif
3883 
3884             // take care of small jobs which never timeout by themselves because
3885             // of a small number of messages. At the end of each job check
3886             // the time used and if we're timeoutting and there is another job
3887             // then interrupt.
3888             if ((elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) || (elapsedTimer.elapsed() < 0)) {
3889                 if (!mViewItemJobs.isEmpty()) {
3890                     return ViewItemJobInterrupted;
3891                 }
3892                 // else it's completed in fact
3893             } // else proceed with the next job
3894 
3895             break;
3896         default:
3897             // This is *really* a BUG
3898             qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result";
3899             Q_ASSERT(false);
3900             break;
3901         }
3902     }
3903 
3904     // no more jobs
3905 
3906     Q_EMIT q->statusMessage(i18nc("@info:status Finished view fill", "Ready"));
3907 
3908     return ViewItemJobCompleted;
3909 }
3910 
viewItemJobStep()3911 void ModelPrivate::viewItemJobStep()
3912 {
3913     // A single step in the View Fill operation.
3914     // This function wraps viewItemJobStepInternal() which does the step job
3915     // and either completes it or stops because of a timeout.
3916     // If the job is stopped then we start a zero-msecs timer to call us
3917     // back and resume the job. Otherwise we're just done.
3918 
3919     mViewItemJobStepStartTime = ::time(nullptr);
3920 
3921     if (mFillStepTimer.isActive()) {
3922         mFillStepTimer.stop();
3923     }
3924 
3925     if (!mStorageModel) {
3926         return; // nothing more to do
3927     }
3928 
3929     // Save the current item in the view as our process may
3930     // cause items to be reparented (and QTreeView will forget the current item in the meantime).
3931     // This machinery is also needed when we're about to remove items from the view in
3932     // a cleanup job: we'll be trying to set as current the item after the one removed.
3933 
3934     QModelIndex currentIndexBeforeStep = mView->currentIndex();
3935     Item *currentItemBeforeStep = currentIndexBeforeStep.isValid() ? static_cast<Item *>(currentIndexBeforeStep.internalPointer()) : nullptr;
3936 
3937     // mCurrentItemToRestoreAfterViewItemJobStep will be zeroed out if it's killed
3938     mCurrentItemToRestoreAfterViewItemJobStep = currentItemBeforeStep;
3939 
3940     // Save the current item position in the viewport as QTreeView fails to keep
3941     // the current item in the sample place when items are added or removed...
3942     QRect rectBeforeViewItemJobStep;
3943 
3944     const bool lockView = mView->isScrollingLocked();
3945 
3946     // This is generally SLOW AS HELL... (so we avoid it if we lock the view and thus don't need it)
3947     if (mCurrentItemToRestoreAfterViewItemJobStep && (!lockView)) {
3948         rectBeforeViewItemJobStep = mView->visualRect(currentIndexBeforeStep);
3949     }
3950 
3951     // FIXME: If the current item is NOT in the view, preserve the position
3952     //        of the top visible item. This will make the view move yet less.
3953 
3954     // Insulate the View from (very likely spurious) "currentChanged()" signals.
3955     mView->ignoreCurrentChanges(true);
3956 
3957     // And go to real work.
3958     switch (viewItemJobStepInternal()) {
3959     case ViewItemJobInterrupted:
3960         // Operation timed out, need to resume in a while
3961         if (!mInLengthyJobBatch) {
3962             mInLengthyJobBatch = true;
3963         }
3964         mFillStepTimer.start(mViewItemJobStepIdleInterval); // this is a single shot timer connected to viewItemJobStep()
3965         // and go dealing with current/selection out of the switch.
3966         break;
3967     case ViewItemJobCompleted:
3968         // done :)
3969 
3970         Q_ASSERT(mModelForItemFunctions); // UI must be no (longer) disconnected in this state
3971 
3972         // Ask the view to remove the eventual busy indications
3973         if (mInLengthyJobBatch) {
3974             mInLengthyJobBatch = false;
3975         }
3976 
3977         if (mLoading) {
3978             mLoading = false;
3979             mView->modelFinishedLoading();
3980             slotApplyFilter();
3981         }
3982 
3983         // Apply pre-selection, if any
3984         if (mPreSelectionMode != PreSelectNone) {
3985             mView->ignoreCurrentChanges(false);
3986 
3987             bool bSelectionDone = false;
3988 
3989             switch (mPreSelectionMode) {
3990             case PreSelectLastSelected:
3991                 // fall down
3992                 break;
3993             case PreSelectFirstUnreadCentered:
3994                 bSelectionDone = mView->selectFirstMessageItem(MessageTypeUnreadOnly, true); // center
3995                 break;
3996             case PreSelectOldestCentered:
3997                 mView->setCurrentMessageItem(mOldestItem, true /* center */);
3998                 bSelectionDone = true;
3999                 break;
4000             case PreSelectNewestCentered:
4001                 mView->setCurrentMessageItem(mNewestItem, true /* center */);
4002                 bSelectionDone = true;
4003                 break;
4004             case PreSelectNone:
4005                 // deal with selection below
4006                 break;
4007             default:
4008                 qCWarning(MESSAGELIST_LOG) << "ERROR: Unrecognized pre-selection mode " << static_cast<int>(mPreSelectionMode);
4009                 break;
4010             }
4011 
4012             if ((!bSelectionDone) && (mPreSelectionMode != PreSelectNone)) {
4013                 // fallback to last selected, if possible
4014                 if (mLastSelectedMessageInFolder) { // we found it in the loading process: select and jump out
4015                     mView->setCurrentMessageItem(mLastSelectedMessageInFolder);
4016                     bSelectionDone = true;
4017                 }
4018             }
4019 
4020             if (bSelectionDone) {
4021                 mLastSelectedMessageInFolder = nullptr;
4022                 mPreSelectionMode = PreSelectNone;
4023                 return; // already taken care of current / selection
4024             }
4025         }
4026         // deal with current/selection out of the switch
4027 
4028         break;
4029     default:
4030         // This is *really* a BUG
4031         qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result";
4032         Q_ASSERT(false);
4033         break;
4034     }
4035 
4036     // Everything else here deals with the selection
4037 
4038     // If UI is disconnected then we don't have anything else to do here
4039     if (!mModelForItemFunctions) {
4040         mView->ignoreCurrentChanges(false);
4041         return;
4042     }
4043 
4044     // Restore current/selection and/or scrollbar position
4045 
4046     if (mCurrentItemToRestoreAfterViewItemJobStep) {
4047         bool stillIgnoringCurrentChanges = true;
4048 
4049         // If the assert below fails then the previously current item got detached
4050         // and didn't get reattached in the step: this should never happen.
4051         Q_ASSERT(mCurrentItemToRestoreAfterViewItemJobStep->isViewable());
4052 
4053         // Check if the current item changed
4054         QModelIndex currentIndexAfterStep = mView->currentIndex();
4055         Item *currentAfterStep = currentIndexAfterStep.isValid() ? static_cast<Item *>(currentIndexAfterStep.internalPointer()) : nullptr;
4056 
4057         if (mCurrentItemToRestoreAfterViewItemJobStep != currentAfterStep) {
4058             // QTreeView lost the current item...
4059             if (mCurrentItemToRestoreAfterViewItemJobStep != currentItemBeforeStep) {
4060                 // Some view job code expects us to actually *change* the current item.
4061                 // This is done by the cleanup step which removes items and tries
4062                 // to set as current the item *after* the removed one, if possible.
4063                 // We need the view to handle the change though.
4064                 stillIgnoringCurrentChanges = false;
4065                 mView->ignoreCurrentChanges(false);
4066             } else {
4067                 // we just have to restore the old current item. The code
4068                 // outside shouldn't have noticed that we lost it (e.g. the message viewer
4069                 // still should have the old message opened). So we don't need to
4070                 // actually notify the view of the restored setting.
4071             }
4072             // Restore it
4073             qCDebug(MESSAGELIST_LOG) << "Gonna restore current here" << mCurrentItemToRestoreAfterViewItemJobStep->subject();
4074             mView->setCurrentIndex(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0));
4075         } else {
4076             // The item we're expected to set as current is already current
4077             if (mCurrentItemToRestoreAfterViewItemJobStep != currentItemBeforeStep) {
4078                 // But we have changed it in the job step.
4079                 // This means that: we have deleted the current item and chosen a
4080                 // new candidate as current but Qt also has chosen it as candidate
4081                 // and already made it current. The problem is that (as of Qt 4.4)
4082                 // it probably didn't select it.
4083                 if (!mView->selectionModel()->hasSelection()) {
4084                     stillIgnoringCurrentChanges = false;
4085                     mView->ignoreCurrentChanges(false);
4086 
4087                     qCDebug(MESSAGELIST_LOG) << "Gonna restore selection here" << mCurrentItemToRestoreAfterViewItemJobStep->subject();
4088 
4089                     QItemSelection selection;
4090                     selection.append(QItemSelectionRange(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0)));
4091                     mView->selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows);
4092                 }
4093             }
4094         }
4095 
4096         // FIXME: If it was selected before the change, then re-select it (it may happen that it's not)
4097         if (!lockView) {
4098             // we prefer to keep the currently selected item steady in the view
4099             QRect rectAfterViewItemJobStep = mView->visualRect(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0));
4100             if (rectBeforeViewItemJobStep.y() != rectAfterViewItemJobStep.y()) {
4101                 // QTreeView lost its position...
4102                 mView->verticalScrollBar()->setValue(mView->verticalScrollBar()->value() + rectAfterViewItemJobStep.y() - rectBeforeViewItemJobStep.y());
4103             }
4104         }
4105 
4106         // and kill the insulation, if not yet done
4107         if (stillIgnoringCurrentChanges) {
4108             mView->ignoreCurrentChanges(false);
4109         }
4110 
4111         return;
4112     }
4113 
4114     // Either there was no current item before, or it was lost in a cleanup step and another candidate for
4115     // current item couldn't be found (possibly empty view)
4116     mView->ignoreCurrentChanges(false);
4117 
4118     if (currentItemBeforeStep) {
4119         // lost in a cleanup..
4120         // tell the view that we have a new current, this time with no insulation
4121         mView->slotSelectionChanged(QItemSelection(), QItemSelection());
4122     }
4123 }
4124 
slotStorageModelRowsInserted(const QModelIndex & parent,int from,int to)4125 void ModelPrivate::slotStorageModelRowsInserted(const QModelIndex &parent, int from, int to)
4126 {
4127     if (parent.isValid()) {
4128         return; // ugh... should never happen
4129     }
4130 
4131     Q_ASSERT(from <= to);
4132 
4133     int count = (to - from) + 1;
4134 
4135     mInvariantRowMapper->modelRowsInserted(from, count);
4136 
4137     // look if no current job is in the middle
4138 
4139     int jobCount = mViewItemJobs.count();
4140 
4141     for (int idx = 0; idx < jobCount; idx++) {
4142         ViewItemJob *job = mViewItemJobs.at(idx);
4143 
4144         if (job->currentPass() != ViewItemJob::Pass1Fill) {
4145             // The job is a cleanup or in a later pass: the storage has been already accessed
4146             // and the messages created... no need to care anymore: the invariant row mapper will do the job.
4147             continue;
4148         }
4149 
4150         if (job->currentIndex() > job->endIndex()) {
4151             // The job finished the Pass1Fill but still waits for the pass indicator to be
4152             // changed. This is unlikely but still may happen if the job has been interrupted
4153             // and then a call to slotStorageModelRowsRemoved() caused it to be forcibly completed.
4154             continue;
4155         }
4156 
4157         //
4158         // The following cases are possible:
4159         //
4160         //               from  to
4161         //                 |    |                              -> shift up job
4162         //               from             to
4163         //                 |              |                    -> shift up job
4164         //               from                            to
4165         //                 |                             |     -> shift up job
4166         //                           from   to
4167         //                             |     |                 -> split job
4168         //                           from                to
4169         //                             |                 |     -> split job
4170         //                                     from      to
4171         //                                       |       |     -> job unaffected
4172         //
4173         //
4174         // FOLDER
4175         // |-------------------------|---------|--------------|
4176         // 0                   currentIndex endIndex         count
4177         //                           +-- job --+
4178         //
4179 
4180         if (from > job->endIndex()) {
4181             // The change is completely above the job, the job is not affected
4182             continue;
4183         }
4184 
4185         if (from > job->currentIndex()) { // and from <= job->endIndex()
4186             // The change starts in the middle of the job in a way that it must be split in two.
4187             // The first part is unaffected by the shift and ranges from job->currentIndex() to from - 1.
4188             // The second part ranges from "from" to job->endIndex() that are now shifted up by count steps.
4189 
4190             // First add a new job for the second part.
4191             auto newJob = new ViewItemJob(from + count, job->endIndex() + count, job->chunkTimeout(), job->idleInterval(), job->messageCheckCount());
4192 
4193             Q_ASSERT(newJob->currentIndex() <= newJob->endIndex());
4194 
4195             idx++; // we can skip this job in the loop, it's already ok
4196             jobCount++; // and our range increases by one.
4197             mViewItemJobs.insert(idx, newJob);
4198 
4199             // Then limit the original job to the first part
4200             job->setEndIndex(from - 1);
4201 
4202             Q_ASSERT(job->currentIndex() <= job->endIndex());
4203 
4204             continue;
4205         }
4206 
4207         // The change starts below (or exactly on the beginning of) the job.
4208         // The job must be shifted up.
4209         job->setCurrentIndex(job->currentIndex() + count);
4210         job->setEndIndex(job->endIndex() + count);
4211 
4212         Q_ASSERT(job->currentIndex() <= job->endIndex());
4213     }
4214 
4215     bool newJobNeeded = true;
4216 
4217     // Try to attach to an existing fill job, if any.
4218     // To enforce consistency we can attach only if the Fill job
4219     // is the last one in the list (might be eventually *also* the first,
4220     // and even being already processed but we must make sure that there
4221     // aren't jobs _after_ it).
4222     if (jobCount > 0) {
4223         ViewItemJob *job = mViewItemJobs.at(jobCount - 1);
4224         if (job->currentPass() == ViewItemJob::Pass1Fill) {
4225             if (
4226                 // The job ends just before the added rows
4227                 (from == (job->endIndex() + 1)) && // The job didn't reach the end of Pass1Fill yet
4228                 (job->currentIndex() <= job->endIndex())) {
4229                 // We can still attach this :)
4230                 job->setEndIndex(to);
4231                 Q_ASSERT(job->currentIndex() <= job->endIndex());
4232                 newJobNeeded = false;
4233             }
4234         }
4235     }
4236 
4237     if (newJobNeeded) {
4238         // FIXME: Should take timing options from aggregation here ?
4239         auto job = new ViewItemJob(from, to, 100, 50, 10);
4240         mViewItemJobs.append(job);
4241     }
4242 
4243     if (!mFillStepTimer.isActive()) {
4244         mFillStepTimer.start(mViewItemJobStepIdleInterval);
4245     }
4246 }
4247 
slotStorageModelRowsRemoved(const QModelIndex & parent,int from,int to)4248 void ModelPrivate::slotStorageModelRowsRemoved(const QModelIndex &parent, int from, int to)
4249 {
4250     // This is called when the underlying StorageModel emits the rowsRemoved signal.
4251 
4252     if (parent.isValid()) {
4253         return; // ugh... should never happen
4254     }
4255 
4256     // look if no current job is in the middle
4257 
4258     Q_ASSERT(from <= to);
4259 
4260     const int count = (to - from) + 1;
4261 
4262     int jobCount = mViewItemJobs.count();
4263 
4264     if (mRootItem && from == 0 && count == mRootItem->childItemCount() && jobCount == 0) {
4265         clear();
4266         return;
4267     }
4268 
4269     for (int idx = 0; idx < jobCount; idx++) {
4270         ViewItemJob *job = mViewItemJobs.at(idx);
4271 
4272         if (job->currentPass() != ViewItemJob::Pass1Fill) {
4273             // The job is a cleanup or in a later pass: the storage has been already accessed
4274             // and the messages created... no need to care: we will invalidate the messages in a while.
4275             continue;
4276         }
4277 
4278         if (job->currentIndex() > job->endIndex()) {
4279             // The job finished the Pass1Fill but still waits for the pass indicator to be
4280             // changed. This is unlikely but still may happen if the job has been interrupted
4281             // and then a call to slotStorageModelRowsRemoved() caused it to be forcibly completed.
4282             continue;
4283         }
4284 
4285         //
4286         // The following cases are possible:
4287         //
4288         //               from  to
4289         //                 |    |                              -> shift down job
4290         //               from             to
4291         //                 |              |                    -> shift down and crop job
4292         //               from                            to
4293         //                 |                             |     -> kill job
4294         //                           from   to
4295         //                             |     |                 -> split job, crop and shift
4296         //                           from                to
4297         //                             |                 |     -> crop job
4298         //                                     from      to
4299         //                                       |       |     -> job unaffected
4300         //
4301         //
4302         // FOLDER
4303         // |-------------------------|---------|--------------|
4304         // 0                   currentIndex endIndex         count
4305         //                           +-- job --+
4306         //
4307 
4308         if (from > job->endIndex()) {
4309             // The change is completely above the job, the job is not affected
4310             continue;
4311         }
4312 
4313         if (from > job->currentIndex()) { // and from <= job->endIndex()
4314             // The change starts in the middle of the job and ends in the middle or after the job.
4315             // The first part is unaffected by the shift and ranges from job->currentIndex() to from - 1
4316             // We use the existing job for this.
4317             job->setEndIndex(from - 1); // stop before the first removed row
4318 
4319             Q_ASSERT(job->currentIndex() <= job->endIndex());
4320 
4321             if (to < job->endIndex()) {
4322                 // The change ends inside the job and a part of it can be completed.
4323 
4324                 // We create a new job for the shifted remaining part. It would actually
4325                 // range from to + 1 up to job->endIndex(), but we need to shift it down by count.
4326                 // since count = ( to - from ) + 1 so from = to + 1 - count
4327 
4328                 auto newJob = new ViewItemJob(from, job->endIndex() - count, job->chunkTimeout(), job->idleInterval(), job->messageCheckCount());
4329 
4330                 Q_ASSERT(newJob->currentIndex() < newJob->endIndex());
4331 
4332                 idx++; // we can skip this job in the loop, it's already ok
4333                 jobCount++; // and our range increases by one.
4334                 mViewItemJobs.insert(idx, newJob);
4335             } // else the change includes completely the end of the job and no other part of it can be completed.
4336 
4337             continue;
4338         }
4339 
4340         // The change starts below (or exactly on the beginning of) the job. ( from <= job->currentIndex() )
4341         if (to >= job->endIndex()) {
4342             // The change completely covers the job: kill it
4343 
4344             // We don't delete the job since we want the other passes to be completed
4345             // This is because the Pass1Fill may have already filled mUnassignedMessageListForPass2
4346             // and may have set mOldestItem and mNewestItem. We *COULD* clear the unassigned
4347             // message list with clearUnassignedMessageLists() but mOldestItem and mNewestItem
4348             // could be still dangling pointers. So we just move the current index of the job
4349             // after the end (so storage model scan terminates) and let it complete spontaneously.
4350             job->setCurrentIndex(job->endIndex() + 1);
4351 
4352             continue;
4353         }
4354 
4355         if (to >= job->currentIndex()) {
4356             // The change partially covers the job. Only a part of it can be completed
4357             // and it must be shifted down. It would actually
4358             // range from to + 1 up to job->endIndex(), but we need to shift it down by count.
4359             // since count = ( to - from ) + 1 so from = to + 1 - count
4360             job->setCurrentIndex(from);
4361             job->setEndIndex(job->endIndex() - count);
4362 
4363             Q_ASSERT(job->currentIndex() <= job->endIndex());
4364 
4365             continue;
4366         }
4367 
4368         // The change is completely below the job: it must be shifted down.
4369         job->setCurrentIndex(job->currentIndex() - count);
4370         job->setEndIndex(job->endIndex() - count);
4371     }
4372 
4373     // This will invalidate the ModelInvariantIndex-es that have been removed and return
4374     // them all in a nice list that we can feed to a view removal job.
4375     auto invalidatedIndexes = mInvariantRowMapper->modelRowsRemoved(from, count);
4376 
4377     if (invalidatedIndexes) {
4378         // Try to attach to an existing cleanup job, if any.
4379         // To enforce consistency we can attach only if the Cleanup job
4380         // is the last one in the list (might be eventually *also* the first,
4381         // and even being already processed but we must make sure that there
4382         // aren't jobs _after_ it).
4383         if (jobCount > 0) {
4384             ViewItemJob *job = mViewItemJobs.at(jobCount - 1);
4385             if (job->currentPass() == ViewItemJob::Pass1Cleanup) {
4386                 if ((job->currentIndex() <= job->endIndex()) && job->invariantIndexList()) {
4387                     // qCDebug(MESSAGELIST_LOG) << "Appending " << invalidatedIndexes->count() << " invalidated indexes to existing cleanup job";
4388                     // We can still attach this :)
4389                     *(job->invariantIndexList()) += *invalidatedIndexes;
4390                     job->setEndIndex(job->endIndex() + invalidatedIndexes->count());
4391                     delete invalidatedIndexes;
4392                     invalidatedIndexes = nullptr;
4393                 }
4394             }
4395         }
4396 
4397         if (invalidatedIndexes) {
4398             // Didn't append to any existing cleanup job.. create a new one
4399 
4400             // qCDebug(MESSAGELIST_LOG) << "Creating new cleanup job for " << invalidatedIndexes->count() << " invalidated indexes";
4401             // FIXME: Should take timing options from aggregation here ?
4402             auto job = new ViewItemJob(ViewItemJob::Pass1Cleanup, invalidatedIndexes, 100, 50, 10);
4403             mViewItemJobs.append(job);
4404         }
4405 
4406         if (!mFillStepTimer.isActive()) {
4407             mFillStepTimer.start(mViewItemJobStepIdleInterval);
4408         }
4409     }
4410 }
4411 
slotStorageModelLayoutChanged()4412 void ModelPrivate::slotStorageModelLayoutChanged()
4413 {
4414     qCDebug(MESSAGELIST_LOG) << "Storage model layout changed";
4415     // need to reset everything...
4416     q->setStorageModel(mStorageModel);
4417     qCDebug(MESSAGELIST_LOG) << "Storage model layout changed done";
4418 }
4419 
slotStorageModelDataChanged(const QModelIndex & fromIndex,const QModelIndex & toIndex)4420 void ModelPrivate::slotStorageModelDataChanged(const QModelIndex &fromIndex, const QModelIndex &toIndex)
4421 {
4422     Q_ASSERT(mStorageModel); // must exist (and be the sender of the signal connected to this slot)
4423 
4424     int from = fromIndex.row();
4425     int to = toIndex.row();
4426 
4427     Q_ASSERT(from <= to);
4428 
4429     int count = (to - from) + 1;
4430 
4431     int jobCount = mViewItemJobs.count();
4432 
4433     // This will find out the ModelInvariantIndex-es that need an update and will return
4434     // them all in a nice list that we can feed to a view removal job.
4435     auto indexesThatNeedUpdate = mInvariantRowMapper->modelIndexRowRangeToModelInvariantIndexList(from, count);
4436 
4437     if (indexesThatNeedUpdate) {
4438         // Try to attach to an existing update job, if any.
4439         // To enforce consistency we can attach only if the Update job
4440         // is the last one in the list (might be eventually *also* the first,
4441         // and even being already processed but we must make sure that there
4442         // aren't jobs _after_ it).
4443         if (jobCount > 0) {
4444             ViewItemJob *job = mViewItemJobs.at(jobCount - 1);
4445             if (job->currentPass() == ViewItemJob::Pass1Update) {
4446                 if ((job->currentIndex() <= job->endIndex()) && job->invariantIndexList()) {
4447                     // We can still attach this :)
4448                     *(job->invariantIndexList()) += *indexesThatNeedUpdate;
4449                     job->setEndIndex(job->endIndex() + indexesThatNeedUpdate->count());
4450                     delete indexesThatNeedUpdate;
4451                     indexesThatNeedUpdate = nullptr;
4452                 }
4453             }
4454         }
4455 
4456         if (indexesThatNeedUpdate) {
4457             // Didn't append to any existing update job.. create a new one
4458             // FIXME: Should take timing options from aggregation here ?
4459             auto job = new ViewItemJob(ViewItemJob::Pass1Update, indexesThatNeedUpdate, 100, 50, 10);
4460             mViewItemJobs.append(job);
4461         }
4462 
4463         if (!mFillStepTimer.isActive()) {
4464             mFillStepTimer.start(mViewItemJobStepIdleInterval);
4465         }
4466     }
4467 }
4468 
slotStorageModelHeaderDataChanged(Qt::Orientation,int,int)4469 void ModelPrivate::slotStorageModelHeaderDataChanged(Qt::Orientation, int, int)
4470 {
4471     if (mStorageModelContainsOutboundMessages != mStorageModel->containsOutboundMessages()) {
4472         mStorageModelContainsOutboundMessages = mStorageModel->containsOutboundMessages();
4473         Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount());
4474     }
4475 }
4476 
flags(const QModelIndex & index) const4477 Qt::ItemFlags Model::flags(const QModelIndex &index) const
4478 {
4479     if (!index.isValid()) {
4480         return Qt::NoItemFlags;
4481     }
4482 
4483     Q_ASSERT(d->mModelForItemFunctions); // UI must be connected if a valid index was queried
4484 
4485     Item *it = static_cast<Item *>(index.internalPointer());
4486 
4487     Q_ASSERT(it);
4488 
4489     if (it->type() == Item::GroupHeader) {
4490         return Qt::ItemIsEnabled;
4491     }
4492 
4493     Q_ASSERT(it->type() == Item::Message);
4494 
4495     if (!static_cast<MessageItem *>(it)->isValid()) {
4496         return Qt::NoItemFlags; // not enabled, not selectable
4497     }
4498 
4499     if (static_cast<MessageItem *>(it)->aboutToBeRemoved()) {
4500         return Qt::NoItemFlags; // not enabled, not selectable
4501     }
4502 
4503     if (static_cast<MessageItem *>(it)->status().isDeleted()) {
4504         return Qt::NoItemFlags; // not enabled, not selectable
4505     }
4506 
4507     return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
4508 }
4509 
mimeData(const QModelIndexList & indexes) const4510 QMimeData *MessageList::Core::Model::mimeData(const QModelIndexList &indexes) const
4511 {
4512     QVector<MessageItem *> msgs;
4513     for (const QModelIndex &idx : indexes) {
4514         if (idx.isValid()) {
4515             Item *item = static_cast<Item *>(idx.internalPointer());
4516             if (item->type() == MessageList::Core::Item::Message) {
4517                 msgs << static_cast<MessageItem *>(idx.internalPointer());
4518             }
4519         }
4520     }
4521     return storageModel()->mimeData(msgs);
4522 }
4523 
rootItem() const4524 Item *Model::rootItem() const
4525 {
4526     return d->mRootItem;
4527 }
4528 
isLoading() const4529 bool Model::isLoading() const
4530 {
4531     return d->mLoading;
4532 }
4533 
messageItemByStorageRow(int row) const4534 MessageItem *Model::messageItemByStorageRow(int row) const
4535 {
4536     if (!d->mStorageModel) {
4537         return nullptr;
4538     }
4539     auto idx = d->mInvariantRowMapper->modelIndexRowToModelInvariantIndex(row);
4540     if (!idx) {
4541         return nullptr;
4542     }
4543 
4544     return static_cast<MessageItem *>(idx);
4545 }
4546 
createPersistentSet(const QVector<MessageItem * > & items)4547 MessageItemSetReference Model::createPersistentSet(const QVector<MessageItem *> &items)
4548 {
4549     if (!d->mPersistentSetManager) {
4550         d->mPersistentSetManager = new MessageItemSetManager();
4551     }
4552 
4553     MessageItemSetReference ref = d->mPersistentSetManager->createSet();
4554     for (const auto mi : items) {
4555         d->mPersistentSetManager->addMessageItem(ref, mi);
4556     }
4557 
4558     return ref;
4559 }
4560 
persistentSetCurrentMessageItemList(MessageItemSetReference ref)4561 QList<MessageItem *> Model::persistentSetCurrentMessageItemList(MessageItemSetReference ref)
4562 {
4563     if (d->mPersistentSetManager) {
4564         return d->mPersistentSetManager->messageItems(ref);
4565     }
4566     return QList<MessageItem *>();
4567 }
4568 
deletePersistentSet(MessageItemSetReference ref)4569 void Model::deletePersistentSet(MessageItemSetReference ref)
4570 {
4571     if (!d->mPersistentSetManager) {
4572         return;
4573     }
4574 
4575     d->mPersistentSetManager->removeSet(ref);
4576 
4577     if (d->mPersistentSetManager->setCount() < 1) {
4578         delete d->mPersistentSetManager;
4579         d->mPersistentSetManager = nullptr;
4580     }
4581 }
4582 
4583 #include "moc_model.cpp"
4584