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