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

KDE's Doxygen guidelines are available online.