Messagelib

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

KDE's Doxygen guidelines are available online.