Messagelib

model.cpp
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/filter.h"
30#include "core/groupheaderitem.h"
31#include "core/item_p.h"
32#include "core/manager.h"
33#include "core/messageitem.h"
34#include "core/messageitemsetmanager.h"
35#include "core/model_p.h"
36#include "core/modelinvariantrowmapper.h"
37#include "core/storagemodelbase.h"
38#include "core/theme.h"
39#include "core/view.h"
40#include "messagelist_debug.h"
41#include <config-messagelist.h>
42
43#include "MessageCore/StringUtil"
44#include <Akonadi/Item>
45#include <Akonadi/MessageStatus>
46#include <MessageCore/DateFormatter>
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
61using namespace std::chrono_literals;
62
63namespace MessageList
64{
65namespace Core
66{
67Q_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 */
145class ViewItemJob
146{
147public:
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
159private:
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
196public:
197 /**
198 * Creates a "View Fill" operation job
199 */
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 */
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
229 ~ViewItemJob()
230 {
231 delete mInvariantIndexList;
232 }
233
234public:
235 [[nodiscard]] int startIndex() const
236 {
237 return mStartIndex;
238 }
239
240 void setStartIndex(int startIndex)
241 {
242 mStartIndex = startIndex;
243 mCurrentIndex = startIndex;
244 }
245
246 [[nodiscard]] int currentIndex() const
247 {
248 return mCurrentIndex;
249 }
250
251 void setCurrentIndex(int currentIndex)
252 {
253 mCurrentIndex = currentIndex;
254 }
255
256 [[nodiscard]] int endIndex() const
257 {
258 return mEndIndex;
259 }
260
261 void setEndIndex(int endIndex)
262 {
263 mEndIndex = endIndex;
264 }
265
266 [[nodiscard]] Pass currentPass() const
267 {
268 return mCurrentPass;
269 }
270
271 void setCurrentPass(Pass pass)
272 {
273 mCurrentPass = pass;
274 }
275
276 [[nodiscard]] int idleInterval() const
277 {
278 return mIdleInterval;
279 }
280
281 [[nodiscard]] int chunkTimeout() const
282 {
283 return mChunkTimeout;
284 }
285
286 [[nodiscard]] int messageCheckCount() const
287 {
288 return mMessageCheckCount;
289 }
290
291 [[nodiscard]] QList<ModelInvariantIndex *> *invariantIndexList() const
292 {
293 return mInvariantIndexList;
294 }
295
296 [[nodiscard]] bool disconnectUI() const
297 {
298 return mDisconnectUI;
299 }
300};
301} // namespace Core
302} // namespace MessageList
303
304using namespace MessageList::Core;
305
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
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
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
370void Model::setAggregation(const Aggregation *aggregation)
371{
372 d->mAggregation = aggregation;
373 d->mView->setRootIsDecorated((d->mAggregation->grouping() == Aggregation::NoGrouping) && (d->mAggregation->threading() != Aggregation::NoThreading));
374}
375
376void Model::setTheme(const Theme *theme)
377{
378 d->mTheme = theme;
379}
380
381void Model::setSortOrder(const SortOrder *sortOrder)
382{
383 d->mSortOrder = sortOrder;
384}
385
387{
388 return d->mSortOrder;
389}
390
391void Model::setFilter(const Filter *filter)
392{
393 d->mFilter = filter;
394
395 if (d->mFilter) {
396 connect(d->mFilter, &Filter::finished, this, [this]() {
397 d->slotApplyFilter();
398 });
399 }
400
401 d->slotApplyFilter();
402}
403
404void ModelPrivate::slotApplyFilter()
405{
406 auto childList = mRootItem->childItems();
407 if (!childList) {
408 return;
409 }
410
411 QModelIndex idx; // invalid
412
414 for (const auto child : std::as_const(*childList)) {
415 applyFilterToSubtree(child, idx);
416 }
417
419}
420
421bool ModelPrivate::applyFilterToSubtree(Item *item, const QModelIndex &parentIndex)
422{
423 // This function applies the current filter (eventually empty)
424 // to a message tree starting at "item".
425
426 if (!mModelForItemFunctions) {
427 qCWarning(MESSAGELIST_LOG) << "Cannot apply filter, the UI must be not disconnected.";
428 return true;
429 }
430 Q_ASSERT(item); // the item must obviously be valid
431 Q_ASSERT(item->isViewable()); // the item must be viewable
432
433 // Apply to children first
434
435 auto childList = item->childItems();
436
437 bool childrenMatch = false;
438
439 QModelIndex thisIndex = q->index(item, 0);
440
441 if (childList) {
442 for (const auto child : std::as_const(*childList)) {
443 if (applyFilterToSubtree(child, thisIndex)) {
444 childrenMatch = true;
445 }
446 }
447 }
448
449 if (!mFilter) { // empty filter always matches (but does not expand items)
450 mView->setRowHidden(thisIndex.row(), parentIndex, false);
451 return true;
452 }
453
454 if (childrenMatch) {
455 mView->setRowHidden(thisIndex.row(), parentIndex, false);
456
457 if (!mView->isExpanded(thisIndex)) {
458 mView->expand(thisIndex);
459 }
460 return true;
461 }
462
463 if (item->type() == Item::Message) {
464 if (mFilter->match((MessageItem *)item)) {
465 mView->setRowHidden(thisIndex.row(), parentIndex, false);
466 return true;
467 }
468 } // else this is a group header and it never explicitly matches
469
470 // filter doesn't match, hide the item
471 mView->setRowHidden(thisIndex.row(), parentIndex, true);
472
473 return false;
474}
475
476int Model::columnCount(const QModelIndex &parent) const
477{
478 if (!d->mTheme) {
479 return 0;
480 }
481 if (parent.column() > 0) {
482 return 0;
483 }
484 return d->mTheme->columns().count();
485}
486
487QVariant Model::data(const QModelIndex &index, int role) const
488{
489 /// this is called only when Akonadi is using the selectionmodel
490 /// for item actions. since akonadi uses the ETM ItemRoles, and the
491 /// messagelist uses its own internal roles, here we respond
492 /// to the ETM ones.
493
494 auto item = static_cast<Item *>(index.internalPointer());
495
496 switch (role) {
497 /// taken from entitytreemodel.h
498 case Qt::UserRole + 1: // EntityTreeModel::ItemIdRole
499 if (item->type() == MessageList::Core::Item::Message) {
500 auto mItem = static_cast<MessageItem *>(item);
501 return QVariant::fromValue(mItem->akonadiItem().id());
502 } else {
503 return {};
504 }
505 break;
506 case Qt::UserRole + 2: // EntityTreeModel::ItemRole
507 if (item->type() == MessageList::Core::Item::Message) {
508 auto mItem = static_cast<MessageItem *>(item);
509 return QVariant::fromValue(mItem->akonadiItem());
510 } else {
511 return {};
512 }
513 break;
514 case Qt::UserRole + 3: // EntityTreeModel::MimeTypeRole
515 if (item->type() == MessageList::Core::Item::Message) {
516 return QStringLiteral("message/rfc822");
517 } else {
518 return {};
519 }
520 break;
522 if (item->type() == MessageList::Core::Item::Message) {
523 auto mItem = static_cast<MessageItem *>(item);
524 return mItem->accessibleText(d->mTheme, index.column());
525 } else if (item->type() == MessageList::Core::Item::GroupHeader) {
526 if (index.column() > 0) {
527 return QString();
528 }
529 auto hItem = static_cast<GroupHeaderItem *>(item);
530 return hItem->label();
531 }
532 return QString();
533 break;
534 default:
535 return {};
536 }
537}
538
539QVariant Model::headerData(int section, Qt::Orientation, int role) const
540{
541 if (!d->mTheme) {
542 return {};
543 }
544
545 auto column = d->mTheme->column(section);
546 if (!column) {
547 return {};
548 }
549
550 if (d->mStorageModel && column->isSenderOrReceiver() && (role == Qt::DisplayRole)) {
551 if (d->mStorageModelContainsOutboundMessages) {
552 return QVariant(i18n("Receiver"));
553 }
554 return QVariant(i18n("Sender"));
555 }
556
557 const bool columnPixmapEmpty(column->pixmapName().isEmpty());
558 if ((role == Qt::DisplayRole) && columnPixmapEmpty) {
559 return QVariant(column->label());
560 } else if ((role == Qt::ToolTipRole) && !columnPixmapEmpty) {
561 return QVariant(column->label());
562 } else if ((role == Qt::DecorationRole) && !columnPixmapEmpty) {
563 return QVariant(QIcon::fromTheme(column->pixmapName()));
564 }
565
566 return {};
567}
568
569QModelIndex Model::index(Item *item, int column) const
570{
571 if (!d->mModelForItemFunctions) {
572 return {}; // called with disconnected UI: the item isn't known on the Qt side, yet
573 }
574
575 if (!item) {
576 return {};
577 }
578 // FIXME: This function is a bottleneck (the caching in indexOfChildItem only works 30% of the time)
579 auto par = item->parent();
580 if (!par) {
581 if (item != d->mRootItem) {
582 item->dump(QString());
583 }
584 return {};
585 }
586
587 const int index = par->indexOfChildItem(item);
588 if (index < 0) {
589 return {}; // BUG
590 }
591 return createIndex(index, column, item);
592}
593
594QModelIndex Model::index(int row, int column, const QModelIndex &parent) const
595{
596 if (!d->mModelForItemFunctions) {
597 return {}; // called with disconnected UI: the item isn't known on the Qt side, yet
598 }
599
600#ifdef READD_THIS_IF_YOU_WANT_TO_PASS_MODEL_TEST
601 if (column < 0) {
602 return QModelIndex(); // senseless column (we could optimize by skipping this check but ModelTest from trolltech is pedantic)
603 }
604#endif
605
606 const Item *item;
607 if (parent.isValid()) {
608 item = static_cast<const Item *>(parent.internalPointer());
609 if (!item) {
610 return {}; // should never happen
611 }
612 } else {
613 item = d->mRootItem;
614 }
615
616 if (parent.column() > 0) {
617 return {}; // parent column is not 0: shouldn't have children (as per Qt documentation)
618 }
619
620 Item *child = item->childItem(row);
621 if (!child) {
622 return {}; // no such row in parent
623 }
624 return createIndex(row, column, child);
625}
626
627QModelIndex Model::parent(const QModelIndex &modelIndex) const
628{
629 Q_ASSERT(d->mModelForItemFunctions); // should be never called with disconnected UI
630
631 if (!modelIndex.isValid()) {
632 return {}; // should never happen
633 }
634 auto item = static_cast<Item *>(modelIndex.internalPointer());
635 if (!item) {
636 return {};
637 }
638 auto par = item->parent();
639 if (!par) {
640 return {}; // should never happen
641 }
642 // return index( par, modelIndex.column() );
643 return index(par, 0); // parents are always in column 0 (as per Qt documentation)
644}
645
646int Model::rowCount(const QModelIndex &parent) const
647{
648 if (!d->mModelForItemFunctions) {
649 return 0; // called with disconnected UI
650 }
651
652 const Item *item;
653 if (parent.isValid()) {
654 item = static_cast<const Item *>(parent.internalPointer());
655 if (!item) {
656 return 0; // should never happen
657 }
658 } else {
659 item = d->mRootItem;
660 }
661
662 if (!item->isViewable()) {
663 return 0;
664 }
665
666 return item->childItemCount();
667}
668
669class RecursionPreventer
670{
671public:
672 RecursionPreventer(int &counter)
673 : mCounter(counter)
674 {
675 mCounter++;
676 }
677
678 ~RecursionPreventer()
679 {
680 mCounter--;
681 }
682
683 [[nodiscard]] bool isRecursive() const
684 {
685 return mCounter > 1;
686 }
687
688private:
689 int &mCounter;
690};
691
693{
694 return d->mStorageModel;
695}
696
697void ModelPrivate::clear()
698{
699 q->beginResetModel();
700 if (mFillStepTimer.isActive()) {
701 mFillStepTimer.stop();
702 }
703
704 // Kill pre-selection at this stage
705 mPreSelectionMode = PreSelectNone;
706 mLastSelectedMessageInFolder = nullptr;
707 mOldestItem = nullptr;
708 mNewestItem = nullptr;
709
710 // Reset the row mapper before removing items
711 // This is faster since the items don't need to access the mapper.
712 mInvariantRowMapper->modelReset();
713
714 clearJobList();
715 clearUnassignedMessageLists();
716 clearOrphanChildrenHash();
717 mGroupHeaderItemHash.clear();
718 mGroupHeadersThatNeedUpdate.clear();
719 mThreadingCacheMessageIdMD5ToMessageItem.clear();
720 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.clear();
721 clearThreadingCacheReferencesIdMD5ToMessageItem();
722 clearThreadingCacheMessageSubjectMD5ToMessageItem();
723 mViewItemJobStepChunkTimeout = 100;
724 mViewItemJobStepIdleInterval = 10;
725 mViewItemJobStepMessageCheckCount = 10;
726 delete mPersistentSetManager;
727 mPersistentSetManager = nullptr;
728 mCurrentItemToRestoreAfterViewItemJobStep = nullptr;
729
730 mTodayDate = QDate::currentDate();
731
732 // FIXME: CLEAR THE FILTER HERE AS WE CAN'T APPLY IT WITH UI DISCONNECTED!
733
734 mRootItem->killAllChildItems();
735
736 q->endResetModel();
737 // Q_EMIT headerDataChanged();
738
739 mView->selectionModel()->clearSelection();
740}
741
742void Model::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode)
743{
744 // Prevent a case of recursion when opening a folder that has a message and the folder was
745 // never opened before.
746 RecursionPreventer preventer(d->mRecursionCounterForReset);
747 if (preventer.isRecursive()) {
748 return;
749 }
750
751 d->clear();
752
753 if (d->mStorageModel) {
754 // Disconnect all signals from old storageModel
755 std::for_each(d->mStorageModelConnections.cbegin(), d->mStorageModelConnections.cend(), [](const QMetaObject::Connection &c) -> bool {
756 return QObject::disconnect(c);
757 });
758 d->mStorageModelConnections.clear();
759 }
760
761 const bool isReload = (d->mStorageModel == storageModel);
762 d->mStorageModel = storageModel;
763
764 if (!d->mStorageModel) {
765 return; // no folder: nothing to fill
766 }
767
768 // Save threading cache of the previous folder, but only if the cache was
769 // enabled and a different folder is being loaded - reload of the same folder
770 // means change in aggregation in which case we will have to re-build the
771 // cache so there's no point saving the current threading cache.
772 if (d->mThreadingCache.isEnabled() && !isReload) {
773 d->mThreadingCache.save();
774 } else {
775 if (isReload) {
776 qCDebug(MESSAGELIST_LOG) << "Identical folder reloaded, not saving old threading cache";
777 } else {
778 qCDebug(MESSAGELIST_LOG) << "Threading disabled in previous folder, not saving threading cache";
779 }
780 }
781 // Load threading cache for the new folder, but only if threading is enabled,
782 // otherwise we would just be caching a flat list.
783 if (d->mAggregation->threading() != Aggregation::NoThreading) {
784 d->mThreadingCache.setEnabled(true);
785 d->mThreadingCache.load(d->mStorageModel->id(), d->mAggregation);
786 } else {
787 // No threading, no cache - don't even bother inserting entries into the
788 // cache or trying to look them up there
789 d->mThreadingCache.setEnabled(false);
790 qCDebug(MESSAGELIST_LOG) << "Threading disabled in folder" << d->mStorageModel->id() << ", not using threading cache";
791 }
792
793 d->mPreSelectionMode = preSelectionMode;
794 d->mStorageModelContainsOutboundMessages = d->mStorageModel->containsOutboundMessages();
795
796 d->mStorageModelConnections = {connect(d->mStorageModel,
798 this,
799 [this](const QModelIndex &parent, int first, int last) {
800 d->slotStorageModelRowsInserted(parent, first, last);
801 }),
802 connect(d->mStorageModel,
804 this,
805 [this](const QModelIndex &parent, int first, int last) {
806 d->slotStorageModelRowsRemoved(parent, first, last);
807 }),
808 connect(d->mStorageModel,
810 this,
811 [this]() {
812 d->slotStorageModelLayoutChanged();
813 }),
814 connect(d->mStorageModel,
816 this,
817 [this]() {
818 d->slotStorageModelLayoutChanged();
819 }),
820 connect(d->mStorageModel,
822 this,
823 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
824 d->slotStorageModelDataChanged(topLeft, bottomRight);
825 }),
826 connect(d->mStorageModel, &StorageModel::headerDataChanged, this, [this](Qt::Orientation orientation, int first, int last) {
827 d->slotStorageModelHeaderDataChanged(orientation, first, last);
828 })};
829
830 if (d->mStorageModel->rowCount() == 0) {
831 return; // folder empty: nothing to fill
832 }
833
834 // Here we use different strategies based on user preference and the folder size.
835 // The knobs we can tune are:
836 //
837 // - The number of jobs used to scan the whole folder and their order
838 //
839 // There are basically two approaches to this. One is the "single big job"
840 // approach. It scans the folder from the beginning to the end in a single job
841 // entry. The job passes are done only once. It's advantage is that it's simpler
842 // and it's less likely to generate imperfect parent threadings. The bad
843 // side is that since the folders are "sort of" date ordered then the most interesting
844 // messages show up at the end of the work. Not nice for large folders.
845 // The other approach uses two jobs. This is a bit slower but smarter strategy.
846 // First we scan the latest 1000 messages and *then* take care of the older ones.
847 // This will show up the most interesting messages almost immediately. (Well...
848 // All this assuming that the underlying storage always appends the newly arrived messages)
849 // The strategy is slower since it generates some imperfect parent threadings which must be
850 // adjusted by the second job. For instance, in my kernel mailing list folder this "smart" approach
851 // generates about 150 additional imperfectly threaded children... but the "today"
852 // messages show up almost immediately. The two-chunk job also makes computing
853 // the percentage user feedback a little harder and might break some optimization
854 // in the insertions (we're able to optimize appends and prepends but a chunked
855 // job is likely to split our work at a boundary where messages are always inserted
856 // in the middle of the list).
857 //
858 // - The maximum time to spend inside a single job step
859 //
860 // The larger this time, the greater the number of messages per second that this
861 // engine can process but also greater time with frozen UI -> less interactivity.
862 // Reasonable values start at 50 msecs. Values larger than 300 msecs are very likely
863 // to be perceived by the user as UI non-reactivity.
864 //
865 // - The number of messages processed in each job step subchunk.
866 //
867 // A job subchunk is processed without checking the maximum time above. This means
868 // that each job step will process at least the number of messages specified by this value.
869 // Very low values mean that we respect the maximum time very carefully but we also
870 // waste time to check if we ran out of time :)
871 // Very high values are likely to cause the engine to not respect the maximum step time.
872 // Reasonable values go from 5 to 100.
873 //
874 // - The "idle" time between two steps
875 //
876 // The lower this time, the greater the number of messages per second that this
877 // engine can process but also lower time for the UI to process events -> less interactivity.
878 // A value of 0 here means that Qt will trigger the timer as soon as it has some
879 // idle time to spend. UI events will be still processed but slowdowns are possible.
880 // 0 is reasonable though. Values larger than 200 will tend to make the total job
881 // completion times high.
882 //
883
884 // If we have no filter it seems that we can apply a huge optimization.
885 // We disconnect the UI for the first huge filling job. This allows us
886 // to save the extremely expensive beginInsertRows()/endInsertRows() calls
887 // and call a single layoutChanged() at the end. This slows down a lot item
888 // expansion. But on the other side if only few items need to be expanded
889 // then this strategy is better. If filtering is enabled then this strategy
890 // isn't applicable (because filtering requires interaction with the UI
891 // while the data is loading).
892
893 // So...
894
895 // For the very first small chunk it's ok to work with disconnected UI as long
896 // as we have no filter. The first small chunk is always 1000 messages, so
897 // even if all of them are expanded, it's still somewhat acceptable.
898 bool canDoFirstSmallChunkWithDisconnectedUI = !d->mFilter;
899
900 // Larger works need a bigger condition: few messages must be expanded in the end.
901 bool canDoJobWithDisconnectedUI = // we have no filter
902 !d->mFilter
903 && (
904 // we do no threading at all
905 (d->mAggregation->threading() == Aggregation::NoThreading) || // or we never expand threads
906 (d->mAggregation->threadExpandPolicy() == Aggregation::NeverExpandThreads) || // or we expand threads but we'll be going to expand really only a few
907 (
908 // so we don't expand them all
909 (d->mAggregation->threadExpandPolicy() != Aggregation::AlwaysExpandThreads) && // and we'd expand only a few in fact
910 (d->mStorageModel->initialUnreadRowCountGuess() < 1000)));
911
912 switch (d->mAggregation->fillViewStrategy()) {
914 // favor interactivity
915 if ((!canDoJobWithDisconnectedUI) && (d->mStorageModel->rowCount() > 3000)) { // empiric value
916 // First a small job with the most recent messages. Large chunk, small (but non zero) idle interval
917 // and a larger number of messages to process at once.
918 auto job1 =
919 new ViewItemJob(d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 200, 20, 100, canDoFirstSmallChunkWithDisconnectedUI);
920 d->mViewItemJobs.append(job1);
921 // Then a larger job with older messages. Small chunk, bigger idle interval, small number of messages to
922 // process at once.
923 auto job2 = new ViewItemJob(0, d->mStorageModel->rowCount() - 1001, 100, 50, 10, false);
924 d->mViewItemJobs.append(job2);
925
926 // We could even extremize this by splitting the folder in several
927 // chunks and scanning them from the newest to the oldest... but the overhead
928 // due to imperfectly threaded children would be probably too big.
929 } else {
930 // small folder or can be done with disconnected UI: single chunk work.
931 // Lag the CPU a bit more but not too much to destroy even the earliest interactivity.
932 auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 150, 30, 30, canDoJobWithDisconnectedUI);
933 d->mViewItemJobs.append(job);
934 }
935 break;
937 // More batchy jobs, still interactive to a certain degree
938 if ((!canDoJobWithDisconnectedUI) && (d->mStorageModel->rowCount() > 3000)) { // empiric value
939 // large folder, but favor speed
940 auto job1 =
941 new ViewItemJob(d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoFirstSmallChunkWithDisconnectedUI);
942 d->mViewItemJobs.append(job1);
943 auto job2 = new ViewItemJob(0, d->mStorageModel->rowCount() - 1001, 200, 0, 10, false);
944 d->mViewItemJobs.append(job2);
945 } else {
946 // small folder or can be done with disconnected UI and favor speed: single chunk work.
947 // Lag the CPU more, get more work done
948 auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoJobWithDisconnectedUI);
949 d->mViewItemJobs.append(job);
950 }
951 break;
953 // one large job, never interrupt, block UI
954 auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 60000, 0, 100000, canDoJobWithDisconnectedUI);
955 d->mViewItemJobs.append(job);
956 break;
957 }
958 default:
959 qCWarning(MESSAGELIST_LOG) << "Unrecognized fill view strategy";
960 Q_ASSERT(false);
961 break;
962 }
963
964 d->mLoading = true;
965
966 d->viewItemJobStep();
967}
968
969void ModelPrivate::checkIfDateChanged()
970{
971 // This function is called by MessageList::Core::Manager once in a while (every 1 minute or sth).
972 // It is used to check if the current date has changed (with respect to mTodayDate).
973 //
974 // Our message items cache the formatted dates (as formatting them
975 // on the fly would be too expensive). We also cache the labels of the groups which often display dates.
976 // When the date changes we would need to fix all these strings.
977 //
978 // A dedicated algorithm to refresh the labels of the items would be either too complex
979 // or would block on large trees. Fixing the labels of the groups is also quite hard...
980 //
981 // So to keep the things simple we just reload the view.
982
983 if (!mStorageModel) {
984 return; // nothing to do
985 }
986
987 if (mLoading) {
988 return; // not now
989 }
990
991 if (!mViewItemJobs.isEmpty()) {
992 return; // not now
993 }
994
995 if (mTodayDate == QDate::currentDate()) {
996 return; // date not changed
997 }
998
999 // date changed, reload the view (and try to preserve the current selection)
1000 Manager::instance()->dateFormatter()->invalidateReferenceDate();
1001 q->setStorageModel(mStorageModel, PreSelectLastSelected);
1002}
1003
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
1021void 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
1136void ModelPrivate::clearThreadingCacheReferencesIdMD5ToMessageItem()
1137{
1138 qDeleteAll(mThreadingCacheMessageReferencesIdMD5ToMessageItem);
1139 mThreadingCacheMessageReferencesIdMD5ToMessageItem.clear();
1140}
1141
1142void ModelPrivate::clearThreadingCacheMessageSubjectMD5ToMessageItem()
1143{
1144 qDeleteAll(mThreadingCacheMessageSubjectMD5ToMessageItem);
1145 mThreadingCacheMessageSubjectMD5ToMessageItem.clear();
1146}
1147
1148void ModelPrivate::clearOrphanChildrenHash()
1149{
1150 qDeleteAll(mOrphanChildrenHash);
1151 mOrphanChildrenHash.clear();
1152}
1153
1154void 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
1170void 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()) {
1206 INSERT_GROUP_WITH_COMPARATOR(ItemDateComparator)
1207 break;
1209 INSERT_GROUP_WITH_COMPARATOR(ItemMaxDateComparator)
1210 break;
1212 INSERT_GROUP_WITH_COMPARATOR(ItemSenderOrReceiverComparator)
1213 break;
1215 INSERT_GROUP_WITH_COMPARATOR(ItemSenderComparator)
1216 break;
1218 INSERT_GROUP_WITH_COMPARATOR(ItemReceiverComparator)
1219 break;
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
1244void 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
1264void 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
1293void 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()) {
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 const int startOfWeekDaysAgo = (daysInWeek + mTodayDate.dayOfWeek() - QLocale().firstDayOfWeek()) % daysInWeek;
1335 const 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, QLatin1StringView("yyyy")));
1373 }
1374 break;
1375 }
1376
1378 date = mi->date();
1379 groupLabel = mi->displaySenderOrReceiver();
1380 break;
1381
1383 date = mi->date();
1384 groupLabel = mi->displaySender();
1385 break;
1386
1388 date = mi->date();
1389 groupLabel = mi->displayReceiver();
1390 break;
1391
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()) {
1414 // nothing to do
1415 break;
1417 // expand always
1419 break;
1421 // expand only if "close" to today
1422 if (mViewItemJobStepStartTime > ghi->date()) {
1423 if ((mViewItemJobStepStartTime - ghi->date()) < (3600 * 72)) {
1425 }
1426 } else {
1427 if ((ghi->date() - mViewItemJobStepStartTime) < (3600 * 72)) {
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
1461MessageItem *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 MD5Hash 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
1578void 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
1594void 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
1608class MessageLessThanByDate
1609{
1610public:
1611 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
1624void 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
1650void 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
1679void 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
1707void 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
1739MessageItem *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 auto 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//
1830template<class ItemComparator>
1831static 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
1839bool 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()) {
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;
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;
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;
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;
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()) {
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;
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;
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;
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;
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
2024void 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
2075void 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
2118void 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()) {
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;
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);
2226 mStorageModel->setMessageItemStatus(mi, row, Akonadi::MessageStatus::statusWatched());
2227 } else if (pParent->status().isIgnored()) {
2228 int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(mi);
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()) {
2255 INSERT_MESSAGE_WITH_COMPARATOR(ItemDateComparator)
2256 break;
2258 INSERT_MESSAGE_WITH_COMPARATOR(ItemMaxDateComparator)
2259 break;
2261 INSERT_MESSAGE_WITH_COMPARATOR(ItemSizeComparator)
2262 break;
2264 INSERT_MESSAGE_WITH_COMPARATOR(ItemSenderOrReceiverComparator)
2265 break;
2267 INSERT_MESSAGE_WITH_COMPARATOR(ItemSenderComparator)
2268 break;
2270 INSERT_MESSAGE_WITH_COMPARATOR(ItemReceiverComparator)
2271 break;
2273 INSERT_MESSAGE_WITH_COMPARATOR(ItemSubjectComparator)
2274 break;
2276 INSERT_MESSAGE_WITH_COMPARATOR(ItemActionItemStatusComparator)
2277 break;
2279 INSERT_MESSAGE_WITH_COMPARATOR(ItemUnreadStatusComparator)
2280 break;
2281 case SortOrder::SortMessagesByImportantStatus:
2282 INSERT_MESSAGE_WITH_COMPARATOR(ItemImportantStatusComparator)
2283 break;
2285 INSERT_MESSAGE_WITH_COMPARATOR(ItemAttachmentStatusComparator)
2286 break;
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()) {
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
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;
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;
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
2419ModelPrivate::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()) {
2469 CHECK_IF_GROUP_NEEDS_RESORTING(ItemDateComparator)
2470 break;
2472 CHECK_IF_GROUP_NEEDS_RESORTING(ItemMaxDateComparator)
2473 break;
2475 CHECK_IF_GROUP_NEEDS_RESORTING(ItemSenderOrReceiverComparator)
2476 break;
2478 CHECK_IF_GROUP_NEEDS_RESORTING(ItemSenderComparator)
2479 break;
2481 CHECK_IF_GROUP_NEEDS_RESORTING(ItemReceiverComparator)
2482 break;
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
2517ModelPrivate::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
2558ModelPrivate::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
2639ModelPrivate::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()) {
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;
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
2754ModelPrivate::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()) {
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;
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 auto 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()) {
2945 mightHaveOtherMeansForThreading = mi->subjectIsPrefixed() || !mi->referencesIdMD5().isEmpty();
2946 break;
2948 mightHaveOtherMeansForThreading = !mi->referencesIdMD5().isEmpty();
2949 break;
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
3026ModelPrivate::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()) {
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
3260ModelPrivate::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 const 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
3406ModelPrivate::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
3592namespace Stats
3593{
3594// Number of existing jobs/passes
3595static const int numberOfPasses = ViewItemJob::LastIndex;
3596
3597// The pass in the last call of viewItemJobStepInternal(), used to detect when
3598// a new pass starts
3599static int lastPass = -1;
3600
3601// Total number of messages in the folder
3602static int totalMessages;
3603
3604// Per-Job data
3605static int numElements[numberOfPasses];
3606static int totalTime[numberOfPasses];
3607static int chunks[numberOfPasses];
3608
3609// Time, in msecs for some special operations
3610static int expandingTreeTime;
3611static int layoutChangeTime;
3612
3613// Descriptions of the job, for nicer debug output
3614static 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
3623static QTime firstStartTime;
3624
3625// Timer to track time the current job takes
3626static QTime currentJobStartTime;
3627
3628// Zeros the stats, to be called when the first job starts
3629static 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
3643void 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
3714ModelPrivate::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
3911void 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
4125void 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
4248void 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
4412void 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
4420void 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
4469void 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
4477Qt::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
4508}
4509
4510QMimeData *MessageList::Core::Model::mimeData(const QModelIndexList &indexes) const
4511{
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
4525{
4526 return d->mRootItem;
4527}
4528
4530{
4531 return d->mLoading;
4532}
4533
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
4547MessageItemSetReference Model::createPersistentSet(const QList<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
4562{
4563 if (d->mPersistentSetManager) {
4564 return d->mPersistentSetManager->messageItems(ref);
4565 }
4566 return {};
4567}
4568
4569void 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"
qint32 toQInt32() const
bool isImportant() const
static const MessageStatus statusWatched()
static const MessageStatus statusIgnored()
A set of aggregation options that can be applied to the MessageList::Model in a single shot.
Definition aggregation.h:29
@ GroupBySender
Group by sender, always.
Definition aggregation.h:41
@ NoGrouping
Don't group messages at all.
Definition aggregation.h:37
@ GroupByDateRange
Use smart (thread leader) date ranges ("Today","Yesterday","Last Week"...)
Definition aggregation.h:39
@ GroupBySenderOrReceiver
Group by sender (incoming) or receiver (outgoing) field.
Definition aggregation.h:40
@ GroupByDate
Group the messages by the date of the thread leader.
Definition aggregation.h:38
@ GroupByReceiver
Group by receiver, always.
Definition aggregation.h:42
@ NeverExpandGroups
Never expand groups during a view fill algorithm.
Definition aggregation.h:54
@ ExpandRecentGroups
Makes sense only with GroupByDate or GroupByDateRange.
Definition aggregation.h:55
@ AlwaysExpandGroups
All groups are expanded as they are inserted.
Definition aggregation.h:56
@ NoThreading
Perform no threading at all.
Definition aggregation.h:66
@ PerfectReferencesAndSubject
Thread by all of the above and try to match subjects too.
Definition aggregation.h:69
@ PerfectOnly
Thread by "In-Reply-To" field only.
Definition aggregation.h:67
@ PerfectAndReferences
Thread by "In-Reply-To" and "References" fields.
Definition aggregation.h:68
@ AlwaysExpandThreads
Expand all threads (this might be very slow)
Definition aggregation.h:94
@ ExpandThreadsWithUnreadOrImportantMessages
Expand threads with "hot" messages (this includes new, unread, important, todo)
Definition aggregation.h:95
@ ExpandThreadsWithNewMessages
DEPRECATED. New message status no longer exists.
Definition aggregation.h:92
@ ExpandThreadsWithUnreadMessages
Expand threads with unread messages (this includes new)
Definition aggregation.h:93
@ NeverExpandThreads
Never expand any thread, this is fast.
Definition aggregation.h:91
@ TopmostMessage
The thread grouping is computed from the topmost message (very similar to least recent,...
Definition aggregation.h:79
@ MostRecentMessage
The thread grouping is computed from the most recent message.
Definition aggregation.h:81
@ FavorSpeed
Do larger chunks of work, zero intervals between chunks.
@ FavorInteractivity
Do small chunks of work, small intervals between chunks to allow for UI event processing.
@ BatchNoInteractivity
Do one large chunk, no interactivity at all.
This class is responsible of matching messages that should be displayed in the View.
Definition filter.h:34
A single item of the MessageList tree managed by MessageList::Model.
Definition item.h:36
const QString & receiver() const
Returns the receiver associated to this item.
Definition item.cpp:504
size_t size() const
Returns the size of this item (size of the Message, mainly)
Definition item.cpp:459
bool useReceiver() const
Returns whether sender or receiver is supposed to be displayed.
Definition item.cpp:529
const Akonadi::MessageStatus & status() const
Returns the status associated to this Item.
Definition item.cpp:449
InitialExpandStatus initialExpandStatus() const
The initial expand status we have to honor when attaching to the viewable root.
Definition item.cpp:350
@ Message
This item is a MessageItem.
Definition item.h:46
@ InvisibleRoot
This item is just Item and it's the only InvisibleRoot per Model.
Definition item.h:47
@ GroupHeader
This item is a GroupHeaderItem.
Definition item.h:45
void initialSetup(time_t date, size_t size, const QString &sender, const QString &receiver, bool useReceiver)
This is meant to be called right after the constructor.
Definition item.cpp:554
bool hasAncestor(const Item *it) const
Return true if Item pointed by it is an ancestor of this item (that is, if it is its parent,...
Definition item.cpp:365
time_t date() const
Returns the date of this item.
Definition item.cpp:469
time_t maxDate() const
Returns the maximum date in the subtree originating from this item.
Definition item.cpp:479
@ ExpandNeeded
Must expand when this item becomes viewable.
Definition item.h:56
@ NoExpandNeeded
No expand needed at all.
Definition item.h:57
@ ExpandExecuted
Item already expanded.
Definition item.h:58
Type type() const
Returns the type of this item.
Definition item.cpp:345
const QString & sender() const
Returns the sender associated to this item.
Definition item.cpp:489
QString displayReceiver() const
Display receiver.
Definition item.cpp:514
void setStatus(Akonadi::MessageStatus status)
Sets the status associated to this Item.
Definition item.cpp:454
void takeChildItem(Model *model, Item *child)
Removes a child from this item's child list without deleting it.
Definition item.cpp:640
QString displaySenderOrReceiver() const
Display sender or receiver.
Definition item.cpp:524
Item * parent() const
Returns the parent Item in the tree, or 0 if this item isn't attached to the tree.
Definition item.cpp:439
void setParent(Item *pParent)
Sets the parent for this item.
Definition item.cpp:444
void setInitialExpandStatus(InitialExpandStatus initialExpandStatus)
Set the initial expand status we have to honor when attaching to the viewable root.
Definition item.cpp:355
int childItemCount() const
Returns the number of children of this Item.
Definition item.cpp:157
const QString & subject() const
Returns the subject associated to this Item.
Definition item.cpp:534
QString displaySender() const
Display sender.
Definition item.cpp:499
bool isViewable() const
Is this item attached to the viewable root ?
Definition item.cpp:360
Compact storage of the result of an MD5 hash computation, for use in the threading code.
Definition md5hash.h:18
This class manages sets of messageitem references.
The MessageItem class.
Definition messageitem.h:36
@ NonThreadable
this message does not look as being threadable
Definition messageitem.h:65
@ ImperfectParentFound
this message found an imperfect parent to attach to (might be fixed later)
Definition messageitem.h:63
@ ParentMissing
this message might belong to a thread but its parent is actually missing
Definition messageitem.h:64
@ PerfectParentFound
this message found a perfect parent to attach to
Definition messageitem.h:62
This class is an optimizing helper for dealing with large flat QAbstractItemModel objects.
void setTheme(const Theme *theme)
Sets the Theme.
Definition model.cpp:376
QMimeData * mimeData(const QModelIndexList &indexes) const override
Called when user initiates a drag from the messagelist.
Definition model.cpp:4510
void setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode=PreSelectLastSelected)
Sets the storage model from that the messages to be displayed should be fetched.
Definition model.cpp:742
MessageItem * messageItemByStorageRow(int row) const
Returns the message item that is at the current storage row index or zero if no such storage item is ...
Definition model.cpp:4534
QList< MessageItem * > persistentSetCurrentMessageItemList(MessageItemSetReference ref)
Returns the list of MessageItems that are still existing in the set pointed by the specified referenc...
Definition model.cpp:4561
StorageModel * storageModel() const
Returns the StorageModel currently set.
Definition model.cpp:692
MessageItemSetReference createPersistentSet(const QList< MessageItem * > &items)
Creates a persistent set for the specified MessageItems and returns its reference.
Definition model.cpp:4547
Item * rootItem() const
Returns the hidden root item that all the messages are (or will be) attached to.
Definition model.cpp:4524
void setAggregation(const Aggregation *aggregation)
Sets the Aggregation mode.
Definition model.cpp:370
void setPreSelectionMode(PreSelectionMode preSelect)
Sets the pre-selection mode.
Definition model.cpp:1004
~Model() override
Destroys the mighty model along with the tree of items it manages.
Definition model.cpp:352
void setSortOrder(const SortOrder *sortOrder)
Sets the sort order.
Definition model.cpp:381
Model(View *pParent)
Creates the mighty Model attached to the specified View.
Definition model.cpp:306
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
Definition model.cpp:487
const SortOrder * sortOrder() const
Returns the sort order.
Definition model.cpp:386
bool isLoading() const
Returns true if the view is currently loading, that is it's in the first (possibly lengthy) job batch...
Definition model.cpp:4529
void setFilter(const Filter *filter)
Sets the Filter to be applied on messages.
Definition model.cpp:391
void deletePersistentSet(MessageItemSetReference ref)
Deletes the persistent set pointed by the specified reference.
Definition model.cpp:4569
A class which holds information about sorting, e.g.
Definition sortorder.h:23
@ SortGroupsBySenderOrReceiver
Sort groups by sender or receiver (makes sense only with GroupBySenderOrReceiver)
Definition sortorder.h:39
@ SortGroupsBySender
Sort groups by sender (makes sense only with GroupBySender)
Definition sortorder.h:40
@ SortGroupsByReceiver
Sort groups by receiver (makes sense only with GroupByReceiver)
Definition sortorder.h:41
@ SortGroupsByDateTimeOfMostRecent
Sort groups by date/time of the most recent message.
Definition sortorder.h:38
@ NoGroupSorting
Don't sort the groups at all, add them as they come in.
Definition sortorder.h:36
@ SortGroupsByDateTime
Sort groups by date/time of the group.
Definition sortorder.h:37
SortDirection
The "generic" sort direction: used for groups and for messages If you add values here please look at ...
Definition sortorder.h:50
@ SortMessagesBySize
Sort the messages by size.
Definition sortorder.h:68
@ SortMessagesByAttachmentStatus
Sort the messages By "Important" flags of status.
Definition sortorder.h:72
@ SortMessagesByDateTime
Sort the messages by date and time.
Definition sortorder.h:62
@ SortMessagesByActionItemStatus
Sort the messages by the "Action Item" flag of status.
Definition sortorder.h:69
@ NoMessageSorting
Don't sort the messages at all.
Definition sortorder.h:61
@ SortMessagesBySenderOrReceiver
Sort the messages by sender or receiver.
Definition sortorder.h:64
@ SortMessagesBySender
Sort the messages by sender.
Definition sortorder.h:65
@ SortMessagesByReceiver
Sort the messages by receiver.
Definition sortorder.h:66
@ SortMessagesByDateTimeOfMostRecent
Sort the messages by date and time of the most recent message in subtree.
Definition sortorder.h:63
@ SortMessagesByUnreadStatus
Sort the messages by the "Unread" flags of status.
Definition sortorder.h:70
@ SortMessagesBySubject
Sort the messages by subject.
Definition sortorder.h:67
The QAbstractItemModel based interface that you need to provide for your storage to work with Message...
@ PerfectThreadingOnly
Only the data for messageIdMD5 and inReplyToMD5 is needed.
@ PerfectThreadingReferencesAndSubject
All of the above plus subject stuff.
@ PerfectThreadingPlusReferences
messageIdMD5, inReplyToMD5, referencesIdMD5
The Theme class defines the visual appearance of the MessageList.
Definition theme.h:48
The MessageList::View is the real display of the message list.
Definition view.h:48
Q_SCRIPTABLE CaptureState status()
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADICORE_EXPORT Collection fromIndex(const QModelIndex &index)
char * toString(const EngineQuery &query)
bool isValid(QStringView ifopt)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
const QList< QKeySequence > & end()
The implementation independent part of the MessageList library.
Definition aggregation.h:22
PreSelectionMode
Pre-selection is the action of automatically selecting a message just after the folder has finished l...
QModelIndex createIndex(int row, int column, const void *ptr) const const
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
void headerDataChanged(Qt::Orientation orientation, int first, int last)
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsRemoved(const QModelIndex &parent, int first, int last)
char * data()
QDate currentDate()
int dayOfWeek() const const
qint64 daysTo(QDate d) const const
bool isValid(int year, int month, int day)
int month() const const
int year() const const
QDate date() const const
void setSecsSinceEpoch(qint64 secs)
qint64 elapsed() const const
void restoreOverrideCursor()
void setOverrideCursor(const QCursor &cursor)
QIcon fromTheme(const QString &name)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
void clear()
iterator end()
QString standaloneDayName(int day, FormatType type) const const
QString standaloneMonthName(int month, FormatType type) const const
QLocale system()
QString toString(QDate date, FormatType format) const const
int column() const const
void * internalPointer() const const
bool isValid() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QList< QQuickItem * > childItems() const const
int y() const const
void clear()
iterator insert(const T &value)
QByteArray toUtf8() const const
WaitCursor
UserRole
typedef ItemFlags
Orientation
void timeout()
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:59:12 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.