Prepare for inheriting private classes.
[kdepim.git] / messagelist / core / model.cpp
blobac3c3f47677c973e75bfc3d096be8872dfb76797
1 /******************************************************************************
3 * Copyright 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 *******************************************************************************/
22 // This class is a rather huge monster. It's something that resembles a QAbstractItemModel
23 // (because it has to provide the interface for a QTreeView) but isn't entirely one
24 // (for optimization reasons). It basically manages a tree of items of two types:
25 // GroupHeaderItem and MessageItem. Be sure to read the docs for ViewItemJob.
27 // A huge credit here goes to Till Adam which seems to have written most
28 // (if not all) of the original KMail threading code. The KMHeaders implementation,
29 // the documentation and his clever ideas were my starting points and essential tools.
30 // This is why I'm adding his copyright entry (copied from headeritem.cpp) here even if
31 // he didn't write a byte in this file until now :)
33 // Szymon Tomasz Stefanek, 03 Aug 2008 04:50 (am)
35 // This class contains ideas from:
37 // kmheaders.cpp / kmheaders.h, headeritem.cpp / headeritem.h
38 // Copyright: (c) 2004 Till Adam < adam at kde dot org >
40 #include <config-messagelist.h>
41 #include "core/model.h"
42 #include "core/model_p.h"
43 #include "core/view.h"
44 #include "core/filter.h"
45 #include "core/groupheaderitem.h"
46 #include "core/item_p.h"
47 #include "core/messageitem.h"
48 #include "core/modelinvariantrowmapper.h"
49 #include "core/storagemodelbase.h"
50 #include "core/theme.h"
51 #include "core/delegate.h"
52 #include "core/manager.h"
53 #include "core/messageitemsetmanager.h"
54 #include "core/messageitem.h"
56 #include <akonadi/item.h>
57 #include <akonadi/kmime/messagestatus.h>
58 #include "messagecore/stringutil.h"
60 #include <QApplication>
61 #include <QTimer>
62 #include <QDateTime>
63 #include <QScrollBar>
65 #include <KLocale>
66 #include <KCalendarSystem>
67 #include <KGlobal>
68 #include <KIcon>
69 #include <KDebug>
71 namespace MessageList
74 namespace Core
77 K_GLOBAL_STATIC( QTimer, _k_heartBeatTimer )
79 /**
80 * A job in a "View Fill" or "View Cleanup" or "View Update" task.
82 * For a "View Fill" task a job is a set of messages
83 * that are contiguous in the storage. The set is expressed as a range
84 * of row indexes. The task "sweeps" the storage in the specified
85 * range, creates the appropriate Item instances and places them
86 * in the right position in the tree.
88 * The idea is that in a single instance and for the same StorageModel
89 * the jobs should never "cover" the same message twice. This assertion
90 * is enforced all around this source file.
92 * For a "View Cleanup" task the job is a list of ModelInvariantIndex
93 * objects (that are in fact MessageItem objects) that need to be removed
94 * from the view.
96 * For a "View Update" task the job is a list of ModelInvariantIndex
97 * objects (that are in fact MessageItem objects) that need to be updated.
99 * The interesting fact is that all the tasks need
100 * very similar operations to be performed on the message tree.
102 * For a "View Fill" we have 5 passes.
104 * Pass 1 scans the underlying storage, creates the MessageItem objects
105 * (which are subclasses of ModelInvariantIndex) and retrieves invariant
106 * storage indexes for them. It also builds threading caches and
107 * attempts to do some "easy" threading. If it succeeds in threading
108 * and some conditions apply then it also attaches the items to the view.
109 * Any unattached message is placed in a list.
111 * Pass 2 scans the list of messages that haven't been attached in
112 * the first pass and performs perfect and reference based threading.
113 * Since grouping of messages may depend on the "shape" of the thread
114 * then certain threads aren't attacched to the view yet.
115 * Unassigned messages get stuffed into a list waiting for Pass3
116 * or directly to a list waiting for Pass4 (that is, Pass3 may be skipped
117 * if there is no hope to find an imperfect parent by subject based threading).
119 * Pass 3 scans the list of messages that haven't been attached in
120 * the first and second passes and performs subject based threading.
121 * Since grouping of messages may depend on the "shape" of the thread
122 * then certain threads aren't attacched to the view yet.
123 * Anything unattached gets stuffed into the list waiting for Pass4.
125 * Pass 4 scans the unattached threads and puts them in the appropriate
126 * groups. After this pass nothing is unattached.
128 * Pass 5 eventually re-sorts the groups and removes the empty ones.
130 * For a "View Cleanup" we still have 5 passes.
132 * Pass 1 scans the list of invalidated ModelInvariantIndex-es, casts
133 * them to MessageItem objects and detaches them from the view.
134 * The orphan children of the destroyed items get stuffed in the list
135 * of unassigned messages that has been used also in the "View Fill" task above.
137 * Pass 2, 3, 4 and 5: same as "View Fill", just operating on the "orphaned"
138 * messages that need to be reattached to the view.
140 * For a "View Update" we still have 5 passes.
142 * Pass 1 scans the list of ModelInvariantIndex-es that need an update, casts
143 * them to MessageItem objects and handles the updates from storage.
144 * The updates may cause a regrouping so items might be stuffed in one
145 * of the lists for pass 4 or 5.
147 * Pass 2, 3 and 4 are simply empty.
149 * Pass 5: same as "View Fill", just operating on groups that require updates
150 * after the messages have been moved in pass 1.
152 * That's why we in fact have Pass1Fill, Pass1Cleanup, Pass1Update, Pass2, Pass3, Pass4 and Pass5 below.
153 * Pass1Fill, Pass1Cleanup and Pass1Update are exclusive and all of them proceed with Pass2 when finished.
155 class ViewItemJob
157 public:
158 enum Pass
160 Pass1Fill = 0, ///< Build threading caches, *TRY* to do some threading, try to attach something to the view
161 Pass1Cleanup = 1, ///< Kill messages, build list of orphans
162 Pass1Update = 2, ///< Update messages
163 Pass2 = 3, ///< Thread everything by using caches, try to attach more to the view
164 Pass3 = 4, ///< Do more threading (this time try to guess), try to attach more to the view
165 Pass4 = 5, ///< Attach anything is still unattacched
166 Pass5 = 6, ///< Eventually Re-sort group headers and remove the empty ones
167 LastIndex = 7 ///< Keep this at the end, needed to get the size of the enum
169 private:
170 // Data for "View Fill" jobs
171 int mStartIndex; ///< The first index (in the underlying storage) of this job
172 int mCurrentIndex; ///< The current index (in the underlying storage) of this job
173 int mEndIndex; ///< The last index (in the underlying storage) of this job
175 // Data for "View Cleanup" jobs
176 QList< ModelInvariantIndex * > * mInvariantIndexList; ///< Owned list of shallow pointers
178 // Common data
180 // The maximum time that we can spend "at once" inside viewItemJobStep() (milliseconds)
181 // The bigger this value, the larger chunks of work we do at once and less the time
182 // we loose in "breaking and resuming" the job. On the other side large values tend
183 // to make the view less responsive up to a "freeze" perception if this value is larger
184 // than 2000.
185 int mChunkTimeout;
187 // The interval between two fillView steps. The larger the interval, the more interactivity
188 // we have. The shorter the interval the more work we get done per second.
189 int mIdleInterval;
191 // The minimum number of messages we process in every viewItemJobStep() call
192 // The larger this value the less time we loose in checking the timeout every N messages.
193 // On the other side, making this very large may make the view less responsive
194 // if we're processing very few messages at a time and very high values (say > 10000) may
195 // eventually make our job unbreakable until the end.
196 int mMessageCheckCount;
197 Pass mCurrentPass;
199 // If this parameter is true then this job uses a "disconnected" UI.
200 // It's FAR faster since we don't need to call beginInsertRows()/endInsertRows()
201 // and we simply emit a layoutChanged() at the end. It can be done only as the first
202 // job though: subsequent jobs can't use layoutChanged() as it looses the expanded
203 // state of items.
204 bool mDisconnectUI;
205 public:
207 * Creates a "View Fill" operation job
209 ViewItemJob( int startIndex, int endIndex, int chunkTimeout, int idleInterval, int messageCheckCount, bool disconnectUI = false )
210 : mStartIndex( startIndex ), mCurrentIndex( startIndex ), mEndIndex( endIndex ),
211 mInvariantIndexList( 0 ),
212 mChunkTimeout( chunkTimeout ), mIdleInterval( idleInterval ),
213 mMessageCheckCount( messageCheckCount ), mCurrentPass( Pass1Fill ),
214 mDisconnectUI( disconnectUI ) {};
217 * Creates a "View Cleanup" or "View Update" operation job
219 ViewItemJob( Pass pass, QList< ModelInvariantIndex * > * invariantIndexList, int chunkTimeout, int idleInterval, int messageCheckCount )
220 : mStartIndex( 0 ), mCurrentIndex( 0 ), mEndIndex( invariantIndexList->count() - 1 ),
221 mInvariantIndexList( invariantIndexList ),
222 mChunkTimeout( chunkTimeout ), mIdleInterval( idleInterval ),
223 mMessageCheckCount( messageCheckCount ), mCurrentPass( pass ),
224 mDisconnectUI( false ) {};
226 ~ViewItemJob()
228 delete mInvariantIndexList;
230 public:
231 int startIndex() const
232 { return mStartIndex; };
233 void setStartIndex( int startIndex )
234 { mStartIndex = startIndex; mCurrentIndex = startIndex; };
235 int currentIndex() const
236 { return mCurrentIndex; };
237 void setCurrentIndex( int currentIndex )
238 { mCurrentIndex = currentIndex; };
239 int endIndex() const
240 { return mEndIndex; };
241 void setEndIndex( int endIndex )
242 { mEndIndex = endIndex; };
243 Pass currentPass() const
244 { return mCurrentPass; };
245 void setCurrentPass( Pass pass )
246 { mCurrentPass = pass; };
247 int idleInterval() const
248 { return mIdleInterval; };
249 int chunkTimeout() const
250 { return mChunkTimeout; };
251 int messageCheckCount() const
252 { return mMessageCheckCount; };
253 QList< ModelInvariantIndex * > * invariantIndexList() const
254 { return mInvariantIndexList; };
255 bool disconnectUI() const
256 { return mDisconnectUI; };
259 } // namespace Core
261 } // namespace MessageList
263 using namespace MessageList::Core;
265 Model::Model( View *pParent )
266 : QAbstractItemModel( pParent ), d( new ModelPrivate( this ) )
268 d->mRecursionCounterForReset = 0;
269 d->mStorageModel = 0;
270 d->mView = pParent;
271 d->mAggregation = 0;
272 d->mTheme = 0;
273 d->mSortOrder = 0;
274 d->mFilter = 0;
275 d->mPersistentSetManager = 0;
276 d->mInLengthyJobBatch = false;
277 d->mUniqueIdOfLastSelectedMessageInFolder = 0;
278 d->mLastSelectedMessageInFolder = 0;
279 d->mLoading = false;
281 d->mRootItem = new Item( Item::InvisibleRoot );
282 d->mRootItem->setViewable( 0, true );
284 d->mFillStepTimer.setSingleShot( true );
285 d->mInvariantRowMapper = new ModelInvariantRowMapper();
286 d->mModelForItemFunctions= this;
287 connect( &d->mFillStepTimer, SIGNAL( timeout() ),
288 SLOT( viewItemJobStep() ) );
290 d->mCachedTodayLabel = i18n( "Today" );
291 d->mCachedYesterdayLabel = i18n( "Yesterday" );
292 d->mCachedUnknownLabel = i18nc( "Unknown date",
293 "Unknown" );
294 d->mCachedLastWeekLabel = i18n( "Last Week" );
295 d->mCachedTwoWeeksAgoLabel = i18n( "Two Weeks Ago" );
296 d->mCachedThreeWeeksAgoLabel = i18n( "Three Weeks Ago" );
297 d->mCachedFourWeeksAgoLabel = i18n( "Four Weeks Ago" );
298 d->mCachedFiveWeeksAgoLabel = i18n( "Five Weeks Ago" );
300 d->mCachedWatchedOrIgnoredStatusBits = Akonadi::MessageStatus::statusIgnored().toQInt32() | Akonadi::MessageStatus::statusWatched().toQInt32();
303 connect( _k_heartBeatTimer, SIGNAL(timeout()),
304 this, SLOT(checkIfDateChanged()) );
306 if ( !_k_heartBeatTimer->isActive() ) { // First model starts it
307 _k_heartBeatTimer->start( 60000 ); // 1 minute
311 Model::~Model()
313 setStorageModel( 0 );
315 d->clearJobList();
316 d->mOldestItem = 0;
317 d->mNewestItem = 0;
318 d->clearUnassignedMessageLists();
319 d->clearOrphanChildrenHash();
320 d->clearThreadingCacheMessageSubjectMD5ToMessageItem();
321 delete d->mPersistentSetManager;
322 // Delete the invariant row mapper before removing the items.
323 // It's faster since the items will not need to call the invariant
324 delete d->mInvariantRowMapper;
325 delete d->mRootItem;
327 delete d;
330 void Model::setAggregation( const Aggregation * aggregation )
332 d->mAggregation = aggregation;
333 d->mView->setRootIsDecorated( ( d->mAggregation->grouping() == Aggregation::NoGrouping ) &&
334 ( d->mAggregation->threading() != Aggregation::NoThreading ) );
337 void Model::setTheme( const Theme * theme )
339 d->mTheme = theme;
342 void Model::setSortOrder( const SortOrder * sortOrder )
344 d->mSortOrder = sortOrder;
347 void Model::setFilter( const Filter *filter )
349 d->mFilter = filter;
351 QList< Item * > * childList = d->mRootItem->childItems();
352 if ( !childList )
353 return;
355 QModelIndex idx; // invalid
357 QApplication::setOverrideCursor( Qt::WaitCursor );
359 for ( QList< Item * >::Iterator it = childList->begin(); it != childList->end(); ++it )
360 d->applyFilterToSubtree( *it, idx );
362 QApplication::restoreOverrideCursor();
365 bool ModelPrivate::applyFilterToSubtree( Item * item, const QModelIndex &parentIndex )
367 // This function applies the current filter (eventually empty)
368 // to a message tree starting at "item".
370 Q_ASSERT( mModelForItemFunctions ); // The UI must be not disconnected
371 Q_ASSERT( item ); // the item must obviously be valid
372 Q_ASSERT( item->isViewable() ); // the item must be viewable
374 // Apply to children first
376 QList< Item * > * childList = item->childItems();
378 bool childrenMatch = false;
380 QModelIndex thisIndex = q->index( item, 0 );
382 if ( childList )
384 for ( QList< Item * >::Iterator it = childList->begin(); it != childList->end(); ++it )
386 if ( applyFilterToSubtree( *it, thisIndex ) )
387 childrenMatch = true;
391 if ( !mFilter ) // empty filter always matches (but does not expand items)
393 mView->setRowHidden( thisIndex.row(), parentIndex, false );
394 return true;
397 if ( childrenMatch )
399 mView->setRowHidden( thisIndex.row(), parentIndex, false );
400 #if 0
401 // Expanding parents of matching items is an EXTREMELY desiderable feature... but...
403 // FIXME TrollTech: THIS IS PATHETICALLY SLOW
404 // It can take ~20 minutes on a tree with ~11000 items.
405 // Without this call the same tree is scanned in a couple of seconds.
406 // The complexity growth is almost certainly (close to) exponential.
408 // It ends up in _very_ deep recursive stacks like these:
410 // #0 0x00002b37e1e03f03 in QTreeViewPrivate::viewIndex (this=0xbd9ff0, index=@0x7fffd327a420) at itemviews/qtreeview.cpp:3195
411 // #1 0x00002b37e1e07ea6 in QTreeViewPrivate::layout (this=0xbd9ff0, i=8239) at itemviews/qtreeview.cpp:3013
412 // #2 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8238) at itemviews/qtreeview.cpp:2994
413 // #3 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8237) at itemviews/qtreeview.cpp:2994
414 // #4 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8236) at itemviews/qtreeview.cpp:2994
415 // #5 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8235) at itemviews/qtreeview.cpp:2994
416 // #6 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8234) at itemviews/qtreeview.cpp:2994
417 // #7 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8233) at itemviews/qtreeview.cpp:2994
418 // #8 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8232) at itemviews/qtreeview.cpp:2994
419 // #9 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8231) at itemviews/qtreeview.cpp:2994
420 // #10 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8230) at itemviews/qtreeview.cpp:2994
421 // #11 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8229) at itemviews/qtreeview.cpp:2994
422 // #12 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8228) at itemviews/qtreeview.cpp:2994
423 // #13 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8227) at itemviews/qtreeview.cpp:2994
424 // #14 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8226) at itemviews/qtreeview.cpp:2994
425 // #15 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8225) at itemviews/qtreeview.cpp:2994
426 // #16 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8224) at itemviews/qtreeview.cpp:2994
427 // #17 0x00002b37e1e0810e in QTreeViewPrivate::layout (this=0xbd9ff0, i=8223) at itemviews/qtreeview.cpp:2994
428 // ....
430 // UPDATE: Olivier Goffart seems to have fixed this: re-check and re-enable for qt 4.5.
433 if ( !mView->isExpanded( thisIndex ) )
434 mView->expand( thisIndex );
435 #endif
436 return true;
439 if ( item->type() == Item::Message )
441 if ( mFilter->match( ( MessageItem * )item ) )
443 mView->setRowHidden( thisIndex.row(), parentIndex, false );
444 return true;
446 } // else this is a group header and it never explicitly matches
448 // filter doesn't match, hide the item
449 mView->setRowHidden( thisIndex.row(), parentIndex, true );
451 return false;
454 int Model::columnCount( const QModelIndex & parent ) const
456 if ( !d->mTheme )
457 return 0;
458 if ( parent.column() > 0 )
459 return 0;
460 return d->mTheme->columns().count();
463 QVariant Model::data( const QModelIndex & index, int role ) const
465 /// this is called only when Akonadi is using the selectionmodel
466 /// for item actions. since akonadi uses the ETM ItemRoles, and the
467 /// messagelist uses its own internal roles, here we respond
468 /// to the ETM ones.
470 Item* item = static_cast< Item* >( index.internalPointer() );
472 switch( role ) {
473 /// taken from entitytreemodel.h
474 case Qt::UserRole + 1: //EntityTreeModel::ItemIdRole
475 if( item->type() == MessageList::Core::Item::Message ) {
476 MessageItem* mItem = static_cast<MessageItem*>( item );
477 return QVariant::fromValue( mItem->akonadiItem().id() );
478 } else
479 return QVariant();
480 break;
481 case Qt::UserRole + 2: //EntityTreeModel::ItemRole
482 if( item->type() == MessageList::Core::Item::Message ) {
483 MessageItem* mItem = static_cast<MessageItem*>( item );
484 return QVariant::fromValue( mItem->akonadiItem() );
485 } else
486 return QVariant();
487 break;
488 case Qt::UserRole + 3: //EntityTreeModel::MimeTypeRole
489 if( item->type() == MessageList::Core::Item::Message )
490 return QLatin1String( "message/rfc822" );
491 else
492 return QVariant();
493 break;
494 default:
495 return QVariant();
499 QVariant Model::headerData(int section, Qt::Orientation, int role) const
501 if ( !d->mTheme )
502 return QVariant();
504 Theme::Column * column = d->mTheme->column( section );
505 if ( !column )
506 return QVariant();
508 if ( d->mStorageModel && column->isSenderOrReceiver() &&
509 ( role == Qt::DisplayRole ) )
511 if ( d->mStorageModelContainsOutboundMessages )
512 return QVariant( i18n( "Receiver" ) );
513 return QVariant( i18n( "Sender" ) );
516 if ( ( role == Qt::DisplayRole ) && column->pixmapName().isEmpty() )
517 return QVariant( column->label() );
519 if ( ( role == Qt::ToolTipRole ) && !column->pixmapName().isEmpty() )
520 return QVariant( column->label() );
522 if ( ( role == Qt::DecorationRole ) && !column->pixmapName().isEmpty() )
523 return QVariant( KIcon( column->pixmapName() ) );
525 return QVariant();
528 QModelIndex Model::index( Item *item, int column ) const
530 if ( !d->mModelForItemFunctions )
531 return QModelIndex(); // called with disconnected UI: the item isn't known on the Qt side, yet
533 if ( !item ) {
534 return QModelIndex();
536 // FIXME: This function is a bottleneck
537 Item * par = item->parent();
538 if ( !par )
540 if ( item != d->mRootItem )
541 item->dump(QString());
542 return QModelIndex();
544 int indexGuess = item->indexGuess();
545 if ( par->childItemHasIndex( item, indexGuess ) ) // This is 30% of the bottleneck
546 return createIndex( indexGuess, column, item );
548 indexGuess = par->indexOfChildItem( item ); // This is 60% of the bottleneck
549 if ( indexGuess < 0 )
550 return QModelIndex(); // BUG
552 item->setIndexGuess( indexGuess );
553 return createIndex( indexGuess, column, item );
556 QModelIndex Model::index( int row, int column, const QModelIndex &parent ) const
558 if ( !d->mModelForItemFunctions )
559 return QModelIndex(); // called with disconnected UI: the item isn't known on the Qt side, yet
561 #ifdef READD_THIS_IF_YOU_WANT_TO_PASS_MODEL_TEST
562 if ( column < 0 )
563 return QModelIndex(); // senseless column (we could optimize by skipping this check but ModelTest from trolltech is pedantic)
564 #endif
566 const Item *item;
567 if ( parent.isValid() )
569 item = static_cast< const Item * >( parent.internalPointer() );
570 if ( !item )
571 return QModelIndex(); // should never happen
572 } else {
573 item = d->mRootItem;
576 if ( parent.column() > 0 )
577 return QModelIndex(); // parent column is not 0: shouldn't have children (as per Qt documentation)
579 Item * child = item->childItem( row );
580 if ( !child )
581 return QModelIndex(); // no such row in parent
582 return createIndex( row, column, child );
585 QModelIndex Model::parent( const QModelIndex &modelIndex ) const
587 Q_ASSERT( d->mModelForItemFunctions ); // should be never called with disconnected UI
589 if ( !modelIndex.isValid() )
590 return QModelIndex(); // should never happen
591 Item *item = static_cast< Item * >( modelIndex.internalPointer() );
592 if ( !item )
593 return QModelIndex();
594 Item *par = item->parent();
595 if ( !par )
596 return QModelIndex(); // should never happen
597 //return index( par, modelIndex.column() );
598 return index( par, 0 ); // parents are always in column 0 (as per Qt documentation)
601 int Model::rowCount( const QModelIndex &parent ) const
603 if ( !d->mModelForItemFunctions )
604 return 0; // called with disconnected UI
606 const Item *item;
607 if ( parent.isValid() )
609 item = static_cast< const Item * >( parent.internalPointer() );
610 if ( !item )
611 return 0; // should never happen
612 } else {
613 item = d->mRootItem;
616 if ( !item->isViewable() )
617 return 0;
619 return item->childItemCount();
622 class RecursionPreventer
624 public:
625 RecursionPreventer( int &counter )
626 : mCounter( counter ) { mCounter++; }
627 ~RecursionPreventer() { mCounter--; }
628 bool isRecursive() const { return mCounter > 1; }
630 private:
631 int &mCounter;
634 StorageModel *Model::storageModel() const
636 return d->mStorageModel;
639 void Model::setStorageModel( StorageModel *storageModel, PreSelectionMode preSelectionMode )
641 // Prevent a case of recursion when opening a folder that has a message and the folder was
642 // never opened before.
643 RecursionPreventer preventer( d->mRecursionCounterForReset );
644 if ( preventer.isRecursive() )
645 return;
647 if( d->mFillStepTimer.isActive() )
648 d->mFillStepTimer.stop();
650 // Kill pre-selection at this stage
651 d->mPreSelectionMode = PreSelectNone;
652 d->mUniqueIdOfLastSelectedMessageInFolder = 0;
653 d->mLastSelectedMessageInFolder = 0;
654 d->mOldestItem = 0;
655 d->mNewestItem = 0;
657 // Reset the row mapper before removing items
658 // This is faster since the items don't need to access the mapper.
659 d->mInvariantRowMapper->modelReset();
661 d->clearJobList();
662 d->clearUnassignedMessageLists();
663 d->clearOrphanChildrenHash();
664 d->mGroupHeaderItemHash.clear();
665 d->mGroupHeadersThatNeedUpdate.clear();
666 d->mThreadingCacheMessageIdMD5ToMessageItem.clear();
667 d->mThreadingCacheMessageInReplyToIdMD5ToMessageItem.clear();
668 d->clearThreadingCacheMessageSubjectMD5ToMessageItem();
669 d->mViewItemJobStepChunkTimeout = 100;
670 d->mViewItemJobStepIdleInterval = 10;
671 d->mViewItemJobStepMessageCheckCount = 10;
672 if ( d->mPersistentSetManager )
674 delete d->mPersistentSetManager;
675 d->mPersistentSetManager = 0;
677 d->mTodayDate = QDate::currentDate();
679 if ( d->mStorageModel )
681 disconnect( d->mStorageModel, SIGNAL( rowsInserted( const QModelIndex &, int, int ) ),
682 this, SLOT( slotStorageModelRowsInserted( const QModelIndex &, int, int ) ) );
683 disconnect( d->mStorageModel, SIGNAL( rowsRemoved( const QModelIndex &, int, int ) ),
684 this, SLOT( slotStorageModelRowsRemoved( const QModelIndex &, int, int ) ) );
686 disconnect( d->mStorageModel, SIGNAL( layoutChanged() ),
687 this, SLOT( slotStorageModelLayoutChanged() ) );
688 disconnect( d->mStorageModel, SIGNAL( modelReset() ),
689 this, SLOT( slotStorageModelLayoutChanged() ) );
691 disconnect( d->mStorageModel, SIGNAL( dataChanged( const QModelIndex &, const QModelIndex & ) ),
692 this, SLOT( slotStorageModelDataChanged( const QModelIndex &, const QModelIndex & ) ) );
693 disconnect( d->mStorageModel, SIGNAL( headerDataChanged( Qt::Orientation, int, int ) ),
694 this, SLOT( slotStorageModelHeaderDataChanged( Qt::Orientation, int, int ) ) );
697 d->mRootItem->killAllChildItems();
699 // FIXME: CLEAR THE FILTER HERE AS WE CAN'T APPLY IT WITH UI DISCONNECTED!
701 d->mStorageModel = storageModel;
703 reset();
704 //emit headerDataChanged();
706 d->mView->modelHasBeenReset();
707 d->mView->selectionModel()->clearSelection();
709 if ( !d->mStorageModel )
710 return; // no folder: nothing to fill
712 // Sometimes the folders need to be resurrected...
713 d->mStorageModel->prepareForScan();
715 d->mPreSelectionMode = preSelectionMode;
716 d->mUniqueIdOfLastSelectedMessageInFolder = Manager::instance()->preSelectedMessageForStorageModel( d->mStorageModel );
717 d->mStorageModelContainsOutboundMessages = d->mStorageModel->containsOutboundMessages();
719 connect( d->mStorageModel, SIGNAL( rowsInserted( const QModelIndex &, int, int ) ),
720 this, SLOT( slotStorageModelRowsInserted( const QModelIndex &, int, int ) ) );
721 connect( d->mStorageModel, SIGNAL( rowsRemoved( const QModelIndex &, int, int ) ),
722 this, SLOT( slotStorageModelRowsRemoved( const QModelIndex &, int, int ) ) );
724 connect( d->mStorageModel, SIGNAL( layoutChanged() ),
725 this, SLOT( slotStorageModelLayoutChanged() ) );
726 connect( d->mStorageModel, SIGNAL( modelReset() ),
727 this, SLOT( slotStorageModelLayoutChanged() ) );
729 connect( d->mStorageModel, SIGNAL( dataChanged( const QModelIndex &, const QModelIndex & ) ),
730 this, SLOT( slotStorageModelDataChanged( const QModelIndex &, const QModelIndex & ) ) );
731 connect( d->mStorageModel, SIGNAL( headerDataChanged( Qt::Orientation, int, int ) ),
732 this, SLOT( slotStorageModelHeaderDataChanged( Qt::Orientation, int, int ) ) );
734 if ( d->mStorageModel->rowCount() == 0 )
735 return; // folder empty: nothing to fill
737 // Here we use different strategies based on user preference and the folder size.
738 // The knobs we can tune are:
740 // - The number of jobs used to scan the whole folder and their order
742 // There are basically two approaches to this. One is the "single big job"
743 // approach. It scans the folder from the beginning to the end in a single job
744 // entry. The job passes are done only once. It's advantage is that it's simplier
745 // and it's less likely to generate imperfect parent threadings. The bad
746 // side is that since the folders are "sort of" date ordered then the most interesting
747 // messages show up at the end of the work. Not nice for large folders.
748 // The other approach uses two jobs. This is a bit slower but smarter strategy.
749 // First we scan the latest 1000 messages and *then* take care of the older ones.
750 // This will show up the most interesting messages almost immediately. (Well...
751 // All this assuming that the underlying storage always appends the newly arrived messages)
752 // The strategy is slower since it generates some imperfect parent threadings which must be
753 // adjusted by the second job. For instance, in my kernel mailing list folder this "smart" approach
754 // generates about 150 additional imperfectly threaded children... but the "today"
755 // messages show up almost immediately. The two-chunk job also makes computing
756 // the percentage user feedback a little harder and might break some optimization
757 // in the insertions (we're able to optimize appends and prepends but a chunked
758 // job is likely to split our work at a boundary where messages are always inserted
759 // in the middle of the list).
761 // - The maximum time to spend inside a single job step
763 // The larger this time, the greater the number of messages per second that this
764 // engine can process but also greater time with frozen UI -> less interactivity.
765 // Reasonable values start at 50 msecs. Values larger than 300 msecs are very likely
766 // to be percieved by the user as UI non-reactivity.
768 // - The number of messages processed in each job step subchunk.
770 // A job subchunk is processed without checking the maximum time above. This means
771 // that each job step will process at least the number of messages specified by this value.
772 // Very low values mean that we respect the maximum time very carefully but we also
773 // waste time to check if we ran out of time :)
774 // Very high values are likely to cause the engine to not respect the maximum step time.
775 // Reasonable values go from 5 to 100.
777 // - The "idle" time between two steps
779 // The lower this time, the greater the number of messages per second that this
780 // engine can process but also lower time for the UI to process events -> less interactivity.
781 // A value of 0 here means that Qt will trigger the timer as soon as it has some
782 // idle time to spend. UI events will be still processed but slowdowns are possible.
783 // 0 is reasonable though. Values larger than 200 will tend to make the total job
784 // completion times high.
787 // If we have no filter it seems that we can apply a huge optimization.
788 // We disconnect the UI for the first huge filling job. This allows us
789 // to save the extremely expensive beginInsertRows()/endInsertRows() calls
790 // and call a single layoutChanged() at the end. This slows down a lot item
791 // expansion. But on the other side if only few items need to be expanded
792 // then this strategy is better. If filtering is enabled then this strategy
793 // isn't applicable (because filtering requires interaction with the UI
794 // while the data is loading).
796 // So...
798 // For the very first small chunk it's ok to work with disconnected UI as long
799 // as we have no filter. The first small chunk is always 1000 messages, so
800 // even if all of them are expanded, it's still somewhat acceptable.
801 bool canDoFirstSmallChunkWithDisconnectedUI = !d->mFilter;
803 // Larger works need a bigger condition: few messages must be expanded in the end.
804 bool canDoJobWithDisconnectedUI =
805 // we have no filter
806 !d->mFilter &&
808 // we do no threading at all
809 ( d->mAggregation->threading() == Aggregation::NoThreading ) ||
810 // or we never expand threads
811 ( d->mAggregation->threadExpandPolicy() == Aggregation::NeverExpandThreads ) ||
812 // or we expand threads but we'll be going to expand really only a few
814 // so we don't expand them all
815 ( d->mAggregation->threadExpandPolicy() != Aggregation::AlwaysExpandThreads ) &&
816 // and we'd expand only a few in fact
817 ( d->mStorageModel->initialUnreadRowCountGuess() < 1000 )
821 switch ( d->mAggregation->fillViewStrategy() )
823 case Aggregation::FavorInteractivity:
824 // favor interactivity
825 if ( ( !canDoJobWithDisconnectedUI ) && ( d->mStorageModel->rowCount() > 3000 ) ) // empiric value
827 // First a small job with the most recent messages. Large chunk, small (but non zero) idle interval
828 // and a larger number of messages to process at once.
829 ViewItemJob * job1 = new ViewItemJob( d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 200, 20, 100, canDoFirstSmallChunkWithDisconnectedUI );
830 d->mViewItemJobs.append( job1 );
831 // Then a larger job with older messages. Small chunk, bigger idle interval, small number of messages to
832 // process at once.
833 ViewItemJob * job2 = new ViewItemJob( 0, d->mStorageModel->rowCount() - 1001, 100, 50, 10, false );
834 d->mViewItemJobs.append( job2 );
836 // We could even extremize this by splitting the folder in several
837 // chunks and scanning them from the newest to the oldest... but the overhead
838 // due to imperfectly threaded children would be probably too big.
839 } else {
840 // small folder or can be done with disconnected UI: single chunk work.
841 // Lag the CPU a bit more but not too much to destroy even the earliest interactivity.
842 ViewItemJob * job = new ViewItemJob( 0, d->mStorageModel->rowCount() - 1, 150, 30, 30, canDoJobWithDisconnectedUI );
843 d->mViewItemJobs.append( job );
845 break;
846 case Aggregation::FavorSpeed:
847 // More batchy jobs, still interactive to a certain degree
848 if ( ( !canDoJobWithDisconnectedUI ) && ( d->mStorageModel->rowCount() > 3000 ) ) // empiric value
850 // large folder, but favor speed
851 ViewItemJob * job1 = new ViewItemJob( d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoFirstSmallChunkWithDisconnectedUI );
852 d->mViewItemJobs.append( job1 );
853 ViewItemJob * job2 = new ViewItemJob( 0, d->mStorageModel->rowCount() - 1001, 200, 0, 10, false );
854 d->mViewItemJobs.append( job2 );
855 } else {
856 // small folder or can be done with disconnected UI and favor speed: single chunk work.
857 // Lag the CPU more, get more work done
858 ViewItemJob * job = new ViewItemJob( 0, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoJobWithDisconnectedUI );
859 d->mViewItemJobs.append( job );
861 break;
862 case Aggregation::BatchNoInteractivity:
864 // one large job, never interrupt, block UI
865 ViewItemJob * job = new ViewItemJob( 0, d->mStorageModel->rowCount() - 1, 60000, 0, 100000, canDoJobWithDisconnectedUI );
866 d->mViewItemJobs.append( job );
868 break;
869 default:
870 kWarning() << "Unrecognized fill view strategy";
871 Q_ASSERT( false );
872 break;
875 d->mLoading = true;
877 d->viewItemJobStep();
880 void ModelPrivate::checkIfDateChanged()
882 // This function is called by MessageList::Core::Manager once in a while (every 1 minute or sth).
883 // It is used to check if the current date has changed (with respect to mTodayDate).
885 // Our message items cache the formatted dates (as formatting them
886 // on the fly would be too expensive). We also cache the labels of the groups which often display dates.
887 // When the date changes we would need to fix all these strings.
889 // A dedicated algorithm to refresh the labels of the items would be either too complex
890 // or would block on large trees. Fixing the labels of the groups is also quite hard...
892 // So to keep the things simple we just reload the view.
894 if ( !mStorageModel )
895 return; // nothing to do
897 if ( mLoading )
898 return; // not now
900 if ( !mViewItemJobs.isEmpty() )
901 return; // not now
903 if ( mTodayDate == QDate::currentDate() )
904 return; // date not changed
906 // date changed, reload the view (and try to preserve the current selection)
907 q->setStorageModel( mStorageModel, PreSelectLastSelected );
911 void Model::abortMessagePreSelection()
913 // This is used to abort a message pre-selection before we actually could apply it.
914 d->mPreSelectionMode = PreSelectNone;
915 d->mUniqueIdOfLastSelectedMessageInFolder = 0;
916 d->mLastSelectedMessageInFolder = 0;
919 void Model::activateMessageAfterLoading( unsigned long uniqueIdOfMessage, int row )
921 Q_ASSERT( d->mLoading ); // you did it: read the docs in the header.
923 // Ok. we're still loading.
924 // We can have three cases now.
926 // 1) The message hasn't been read from the storage yet. We don't have a MessageItem for it.
927 // We must then use the pre-selection mechanism to activate the message when loading finishes.
928 // 2) The message has already been read from the storage.
929 // 2a) We're in "disconnected UI" state or the message item is not viewable.
930 // The Qt side of the model/view framework doesn't know about the MessageItem yet.
931 // That is, we can't get a valid QModelIndex for the message.
932 // We again must use the pre-selection method.
933 // 2b) No disconnected UI and MessageItem is viewable. Qt knows about it and we can
934 // get the QModelIndex. We can select it NOW.
936 MessageItem * mi = messageItemByStorageRow( row );
938 if( mi )
940 if( mi->isViewable() && d->mModelForItemFunctions )
942 // No disconnected UI and the MessageItem is viewable. Activate it now.
943 d->mView->setCurrentMessageItem( mi );
945 // Also abort any pending pre-selection.
946 abortMessagePreSelection();
947 return;
951 // Use the pre-selection method.
953 d->mPreSelectionMode = PreSelectLastSelected;
955 d->mUniqueIdOfLastSelectedMessageInFolder = mi ? 0 : uniqueIdOfMessage;
956 d->mLastSelectedMessageInFolder = mi;
961 // The "view fill" algorithm implemented in the functions below is quite smart but also quite complex.
962 // It's governed by the following goals:
964 // - Be flexible: allow different configurations from "unsorted flat list" to a "grouped and threaded
965 // list with different sorting algorightms applied to each aggregation level"
966 // - Be reasonably fast
967 // - Be non blocking: UI shouldn't freeze while the algorithm is running
968 // - Be interruptible: user must be able to abort the execution and just switch to another folder in the middle
971 void ModelPrivate::clearUnassignedMessageLists()
973 // This is a bit tricky...
974 // The three unassigned message lists contain messages that have been created
975 // but not yet attached to the view. There may be two major cases for a message:
976 // - it has no parent -> it must be deleted and it will delete its children too
977 // - it has a parent -> it must NOT be deleted since it will be deleted by its parent.
979 // Sometimes the things get a little complicated since in Pass2 and Pass3
980 // we have transitional states in that the MessageItem object can be in two of these lists.
982 // WARNING: This function does NOT fixup mNewestItem and mOldestItem. If one of these
983 // two messages is in the lists below, it's deleted and the member becomes a dangling pointer.
984 // The caller must ensure that both mNewestItem and mOldestItem are set to 0
985 // and this is enforced in the assert below to avoid errors. This basically means
986 // that this function should be called only when the storage model changes or
987 // when the model is destroyed.
988 Q_ASSERT( ( mOldestItem == 0 ) && ( mNewestItem == 0 ) );
990 QList< MessageItem * >::Iterator it;
992 if ( !mUnassignedMessageListForPass2.isEmpty() )
994 // We're actually in Pass1* or Pass2: everything is mUnassignedMessageListForPass2
995 // Something may *also* be in mUnassignedMessageListForPass3 and mUnassignedMessageListForPass4
996 // but that are duplicates for sure.
998 // We can't just sweep the list and delete parentless items since each delete
999 // could kill children which are somewhere AFTER in the list: accessing the children
1000 // would then lead to a SIGSEGV. We first sweep the list gathering parentless
1001 // items and *then* delete them without accessing the parented ones.
1003 QList< MessageItem * > parentless;
1005 for ( it = mUnassignedMessageListForPass2.begin();
1006 it != mUnassignedMessageListForPass2.end(); ++it )
1008 if( !( *it )->parent() )
1009 parentless.append( *it );
1012 for ( it = parentless.begin(); it != parentless.end(); ++it )
1013 delete *it;
1015 mUnassignedMessageListForPass2.clear();
1016 // Any message these list contain was also in mUnassignedMessageListForPass2
1017 mUnassignedMessageListForPass3.clear();
1018 mUnassignedMessageListForPass4.clear();
1019 return;
1022 // mUnassignedMessageListForPass2 is empty
1024 if ( !mUnassignedMessageListForPass3.isEmpty() )
1026 // We're actually at the very end of Pass2 or inside Pass3
1027 // Pass2 pushes stuff in mUnassignedMessageListForPass3 *or* mUnassignedMessageListForPass4
1028 // Pass3 pushes stuff from mUnassignedMessageListForPass3 to mUnassignedMessageListForPass4
1029 // So if we're in Pass2 then the two lists contain distinct messages but if we're in Pass3
1030 // then the two lists may contain the same messages.
1032 if ( !mUnassignedMessageListForPass4.isEmpty() )
1034 // We're actually in Pass3: the messiest one.
1036 QHash< MessageItem *, MessageItem * > itemsToDelete;
1038 for ( it = mUnassignedMessageListForPass3.begin(); it != mUnassignedMessageListForPass3.end(); ++it )
1040 if( !( *it )->parent() )
1041 itemsToDelete.insert( *it, *it );
1044 for ( it = mUnassignedMessageListForPass4.begin(); it != mUnassignedMessageListForPass4.end(); ++it )
1046 if( !( *it )->parent() )
1047 itemsToDelete.insert( *it, *it );
1050 for ( QHash< MessageItem *, MessageItem * >::Iterator it3 = itemsToDelete.begin(); it3 != itemsToDelete.end(); ++it3 )
1051 delete ( *it3 );
1053 mUnassignedMessageListForPass3.clear();
1054 mUnassignedMessageListForPass4.clear();
1055 return;
1058 // mUnassignedMessageListForPass4 is empty so we must be at the end of a very special kind of Pass2
1059 // We have the same problem as in mUnassignedMessageListForPass2.
1060 QList< MessageItem * > parentless;
1062 for ( it = mUnassignedMessageListForPass3.begin(); it != mUnassignedMessageListForPass3.end(); ++it )
1064 if( !( *it )->parent() )
1065 parentless.append( *it );
1068 for ( it = parentless.begin(); it != parentless.end(); ++it )
1069 delete *it;
1071 mUnassignedMessageListForPass3.clear();
1072 return;
1075 // mUnassignedMessageListForPass3 is empty
1076 if ( !mUnassignedMessageListForPass4.isEmpty() )
1078 // we're in Pass4.. this is easy.
1080 // We have the same problem as in mUnassignedMessageListForPass2.
1081 QList< MessageItem * > parentless;
1083 for ( it = mUnassignedMessageListForPass4.begin(); it != mUnassignedMessageListForPass4.end(); ++it )
1085 if( !( *it )->parent() )
1086 parentless.append( *it );
1089 for ( it = parentless.begin(); it != parentless.end(); ++it )
1090 delete *it;
1092 mUnassignedMessageListForPass4.clear();
1093 return;
1097 void ModelPrivate::clearThreadingCacheMessageSubjectMD5ToMessageItem()
1099 qDeleteAll( mThreadingCacheMessageSubjectMD5ToMessageItem );
1100 mThreadingCacheMessageSubjectMD5ToMessageItem.clear();
1103 void ModelPrivate::clearOrphanChildrenHash()
1105 for ( QHash< MessageItem *, MessageItem * >::Iterator it = mOrphanChildrenHash.begin();
1106 it != mOrphanChildrenHash.end(); ++it )
1108 //Q_ASSERT( !( *it )->parent() ); <-- this assert can actually fail for items that get a temporary parent assigned (to preserve the selection).
1109 delete ( *it );
1111 mOrphanChildrenHash.clear();
1114 void ModelPrivate::clearJobList()
1116 if ( mViewItemJobs.isEmpty() )
1117 return;
1119 if ( mInLengthyJobBatch )
1121 mInLengthyJobBatch = false;
1122 mView->modelJobBatchTerminated();
1125 for( QList< ViewItemJob * >::Iterator it = mViewItemJobs.begin();
1126 it != mViewItemJobs.end() ; ++it )
1127 delete ( *it );
1128 mViewItemJobs.clear();
1130 mModelForItemFunctions = q; // make sure it's true, as there remains no job with disconnected UI
1134 void ModelPrivate::attachGroup( GroupHeaderItem *ghi )
1136 if ( ghi->parent() )
1138 if (
1139 ( ( ghi )->childItemCount() > 0 ) && // has children
1140 ( ghi )->isViewable() && // is actually attached to the viewable root
1141 mModelForItemFunctions && // the UI is not disconnected
1142 mView->isExpanded( q->index( ghi, 0 ) ) // is actually expanded
1144 saveExpandedStateOfSubtree( ghi );
1146 // FIXME: This *WILL* break selection and current index... :/
1148 ghi->parent()->takeChildItem( mModelForItemFunctions, ghi );
1151 ghi->setParent( mRootItem );
1153 // I'm using a macro since it does really improve readability.
1154 // I'm NOT using a helper function since gcc will refuse to inline some of
1155 // the calls because they make this function grow too much.
1156 #define INSERT_GROUP_WITH_COMPARATOR( _ItemComparator ) \
1157 switch( mSortOrder->groupSortDirection() ) \
1159 case SortOrder::Ascending: \
1160 mRootItem->d_ptr->insertChildItem< _ItemComparator, true >( mModelForItemFunctions, ghi ); \
1161 break; \
1162 case SortOrder::Descending: \
1163 mRootItem->d_ptr->insertChildItem< _ItemComparator, false >( mModelForItemFunctions, ghi ); \
1164 break; \
1165 default: /* should never happen... */ \
1166 mRootItem->appendChildItem( mModelForItemFunctions, ghi ); \
1167 break; \
1170 switch( mSortOrder->groupSorting() )
1172 case SortOrder::SortGroupsByDateTime:
1173 INSERT_GROUP_WITH_COMPARATOR( ItemDateComparator )
1174 break;
1175 case SortOrder::SortGroupsByDateTimeOfMostRecent:
1176 INSERT_GROUP_WITH_COMPARATOR( ItemMaxDateComparator )
1177 break;
1178 case SortOrder::SortGroupsBySenderOrReceiver:
1179 INSERT_GROUP_WITH_COMPARATOR( ItemSenderOrReceiverComparator )
1180 break;
1181 case SortOrder::SortGroupsBySender:
1182 INSERT_GROUP_WITH_COMPARATOR( ItemSenderComparator )
1183 break;
1184 case SortOrder::SortGroupsByReceiver:
1185 INSERT_GROUP_WITH_COMPARATOR( ItemReceiverComparator )
1186 break;
1187 case SortOrder::NoGroupSorting:
1188 mRootItem->appendChildItem( mModelForItemFunctions, ghi );
1189 break;
1190 default: // should never happen
1191 mRootItem->appendChildItem( mModelForItemFunctions, ghi );
1192 break;
1195 if ( ghi->initialExpandStatus() == Item::ExpandNeeded ) // this actually is a "non viewable expanded state"
1196 if ( ghi->childItemCount() > 0 )
1197 if ( mModelForItemFunctions ) // the UI is not disconnected
1198 syncExpandedStateOfSubtree( ghi );
1200 // A group header is always viewable, when attached: apply the filter, if we have it.
1201 if ( mFilter )
1203 Q_ASSERT( mModelForItemFunctions ); // UI must be NOT disconnected
1204 // apply the filter to subtree
1205 applyFilterToSubtree( ghi, QModelIndex() );
1209 void ModelPrivate::saveExpandedStateOfSubtree( Item *root )
1211 Q_ASSERT( mModelForItemFunctions ); // UI must be NOT disconnected here
1212 Q_ASSERT( root );
1214 root->setInitialExpandStatus( Item::ExpandNeeded );
1216 QList< Item * > * children = root->childItems();
1217 if ( !children )
1218 return;
1220 for( QList< Item * >::Iterator it = children->begin(); it != children->end(); ++it )
1222 if (
1223 ( ( *it )->childItemCount() > 0 ) && // has children
1224 ( *it )->isViewable() && // is actually attached to the viewable root
1225 mView->isExpanded( q->index( *it, 0 ) ) // is actually expanded
1227 saveExpandedStateOfSubtree( *it );
1231 void ModelPrivate::syncExpandedStateOfSubtree( Item *root )
1233 Q_ASSERT( mModelForItemFunctions ); // UI must be NOT disconnected here
1235 // WE ASSUME that:
1236 // - the item is viewable
1237 // - its initialExpandStatus() is Item::ExpandNeeded
1238 // - 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)
1240 QModelIndex idx = q->index( root, 0 );
1242 //if ( !mView->isExpanded( idx ) ) // this is O(logN!) in Qt.... very ugly... but it should never happen here
1243 mView->expand( idx ); // sync the real state in the view
1244 root->setInitialExpandStatus( Item::ExpandExecuted );
1246 QList< Item * > * children = root->childItems();
1247 if ( !children )
1248 return;
1250 for( QList< Item * >::Iterator it = children->begin(); it != children->end(); ++it )
1252 if ( ( *it )->initialExpandStatus() == Item::ExpandNeeded )
1254 if ( ( *it )->childItemCount() > 0 )
1255 syncExpandedStateOfSubtree( *it );
1260 void ModelPrivate::attachMessageToGroupHeader( MessageItem *mi )
1262 QString groupLabel;
1263 time_t date;
1265 // compute the group header label and the date
1266 switch( mAggregation->grouping() )
1268 case Aggregation::GroupByDate:
1269 case Aggregation::GroupByDateRange:
1271 if ( mAggregation->threadLeader() == Aggregation::MostRecentMessage )
1273 date = mi->maxDate();
1274 } else
1276 date = mi->date();
1279 QDateTime dt;
1280 dt.setTime_t( date );
1281 QDate dDate = dt.date();
1282 const KCalendarSystem *calendar = KGlobal::locale()->calendar();
1283 int daysAgo = -1;
1284 if ( calendar->isValid( dDate ) && calendar->isValid( mTodayDate ) ) {
1285 daysAgo = dDate.daysTo( mTodayDate );
1288 if ( ( daysAgo < 0 ) || // In the future
1289 ( static_cast< uint >( date ) == static_cast< uint >( -1 ) ) ) // Invalid
1291 groupLabel = mCachedUnknownLabel;
1292 } else if( daysAgo == 0 ) // Today
1294 groupLabel = mCachedTodayLabel;
1295 } else if ( daysAgo == 1 ) // Yesterday
1297 groupLabel = mCachedYesterdayLabel;
1298 } else if ( daysAgo > 1 && daysAgo < calendar->daysInWeek( mTodayDate ) ) // Within last seven days
1300 groupLabel = KGlobal::locale()->calendar()->weekDayName( dDate );
1301 } else if ( mAggregation->grouping() == Aggregation::GroupByDate ) { // GroupByDate seven days or more ago
1302 groupLabel = KGlobal::locale()->formatDate( dDate, KLocale::ShortDate );
1303 } else if( ( calendar->month( dDate ) == calendar->month( mTodayDate ) ) && // GroupByDateRange within this month
1304 ( calendar->year( dDate ) == calendar->year( mTodayDate ) ) )
1306 int startOfWeekDaysAgo = ( calendar->daysInWeek( mTodayDate ) + calendar->dayOfWeek( mTodayDate ) -
1307 KGlobal::locale()->weekStartDay() ) % calendar->daysInWeek( mTodayDate );
1308 int weeksAgo = ( ( daysAgo - startOfWeekDaysAgo ) / calendar->daysInWeek( mTodayDate ) ) + 1;
1309 switch( weeksAgo )
1311 case 0: // This week
1312 groupLabel = KGlobal::locale()->calendar()->weekDayName( dDate );
1313 break;
1314 case 1: // 1 week ago
1315 groupLabel = mCachedLastWeekLabel;
1316 break;
1317 case 2:
1318 groupLabel = mCachedTwoWeeksAgoLabel;
1319 break;
1320 case 3:
1321 groupLabel = mCachedThreeWeeksAgoLabel;
1322 break;
1323 case 4:
1324 groupLabel = mCachedFourWeeksAgoLabel;
1325 break;
1326 case 5:
1327 groupLabel = mCachedFiveWeeksAgoLabel;
1328 break;
1329 default: // should never happen
1330 groupLabel = mCachedUnknownLabel;
1332 } else if ( calendar->year( dDate ) == calendar->year( mTodayDate ) ) { // GroupByDateRange within this year
1333 groupLabel = calendar->monthName( dDate );
1334 } else { // GroupByDateRange in previous years
1335 groupLabel = i18nc( "Message Aggregation Group Header: Month name and Year number", "%1 %2", calendar->monthName( dDate ), calendar->yearString( dDate ) );
1337 break;
1340 case Aggregation::GroupBySenderOrReceiver:
1341 date = mi->date();
1342 groupLabel = MessageCore::StringUtil::stripEmailAddr( mi->senderOrReceiver() );
1343 break;
1345 case Aggregation::GroupBySender:
1346 date = mi->date();
1347 groupLabel = MessageCore::StringUtil::stripEmailAddr( mi->sender() );
1348 break;
1350 case Aggregation::GroupByReceiver:
1351 date = mi->date();
1352 groupLabel = MessageCore::StringUtil::stripEmailAddr( mi->receiver() );
1353 break;
1355 case Aggregation::NoGrouping:
1356 // append directly to root
1357 attachMessageToParent( mRootItem, mi );
1358 return;
1360 default:
1361 // should never happen
1362 attachMessageToParent( mRootItem, mi );
1363 return;
1366 GroupHeaderItem * ghi;
1368 ghi = mGroupHeaderItemHash.value( groupLabel, 0 );
1369 if( !ghi )
1371 // not found
1373 ghi = new GroupHeaderItem( groupLabel );
1374 ghi->initialSetup( date, mi->size(), mi->sender(), mi->receiver(), mi->senderOrReceiver() );
1376 switch( mAggregation->groupExpandPolicy() )
1378 case Aggregation::NeverExpandGroups:
1379 // nothing to do
1380 break;
1381 case Aggregation::AlwaysExpandGroups:
1382 // expand always
1383 ghi->setInitialExpandStatus( Item::ExpandNeeded );
1384 break;
1385 case Aggregation::ExpandRecentGroups:
1386 // expand only if "close" to today
1387 if ( mViewItemJobStepStartTime > ghi->date() )
1389 if ( ( mViewItemJobStepStartTime - ghi->date() ) < ( 3600 * 72 ) )
1390 ghi->setInitialExpandStatus( Item::ExpandNeeded );
1391 } else {
1392 if ( ( ghi->date() - mViewItemJobStepStartTime ) < ( 3600 * 72 ) )
1393 ghi->setInitialExpandStatus( Item::ExpandNeeded );
1395 break;
1396 default:
1397 // b0rken
1398 break;
1401 attachMessageToParent( ghi, mi );
1403 attachGroup( ghi ); // this will expand the group if required
1405 mGroupHeaderItemHash.insert( groupLabel, ghi );
1406 } else {
1407 // the group was already there (certainly viewable)
1409 // This function may be also called to re-group a message.
1410 // That is, to eventually find a new group for a message that has changed
1411 // its properties (but was already attacched to a group).
1412 // So it may happen that we find out that in fact re-grouping wasn't really
1413 // needed because the message is already in the correct group.
1414 if ( mi->parent() == ghi )
1415 return; // nothing to be done
1417 attachMessageToParent( ghi, mi );
1421 MessageItem * ModelPrivate::findMessageParent( MessageItem * mi )
1423 Q_ASSERT( mAggregation->threading() != Aggregation::NoThreading ); // caller must take care of this
1425 // This function attempts to find a thread parent for the item "mi"
1426 // which actually may already have a children subtree.
1428 // Forged or plain broken message trees are dangerous here.
1429 // For example, a message tree with circular references like
1431 // Message mi, Id=1, In-Reply-To=2
1432 // Message childOfMi, Id=2, In-Reply-To=1
1434 // is perfectly possible and will cause us to find childOfMi
1435 // as parent of mi. This will then create a loop in the message tree
1436 // (which will then no longer be a tree in fact) and cause us to freeze
1437 // once we attempt to climb the parents. We need to take care of that.
1439 bool bMessageWasThreadable = false;
1440 MessageItem * pParent;
1442 // First of all try to find a "perfect parent", that is the message for that
1443 // we have the ID in the "In-Reply-To" field. This is actually done by using
1444 // MD5 caches of the message ids because of speed. Collisions are very unlikely.
1446 QByteArray md5 = mi->inReplyToIdMD5();
1447 if ( !md5.isEmpty() )
1449 // have an In-Reply-To field MD5
1450 pParent = mThreadingCacheMessageIdMD5ToMessageItem.value( md5, 0 );
1451 if(pParent)
1453 // Take care of circular references
1454 if (
1455 ( mi == pParent ) || // self referencing message
1457 ( mi->childItemCount() > 0 ) && // mi already has children, this is fast to determine
1458 pParent->hasAncestor( mi ) // pParent is in the mi's children tree
1462 kWarning() << "Circular In-Reply-To reference loop detected in the message tree";
1463 mi->setThreadingStatus( MessageItem::NonThreadable );
1464 return 0; // broken message: throw it away
1466 mi->setThreadingStatus( MessageItem::PerfectParentFound );
1467 return pParent; // got a perfect parent for this message
1470 // got no perfect parent
1471 bMessageWasThreadable = true; // but the message was threadable
1474 if ( mAggregation->threading() == Aggregation::PerfectOnly )
1476 mi->setThreadingStatus( bMessageWasThreadable ? MessageItem::ParentMissing : MessageItem::NonThreadable );
1477 return 0; // we're doing only perfect parent matches
1480 // Try to use the "References" field. In fact we have the MD5 of the
1481 // (n-1)th entry in References.
1483 // Original rationale from KMHeaders:
1485 // If we don't have a replyToId, or if we have one and the
1486 // corresponding message is not in this folder, as happens
1487 // if you keep your outgoing messages in an OUTBOX, for
1488 // example, try the list of references, because the second
1489 // to last will likely be in this folder. replyToAuxIdMD5
1490 // contains the second to last one.
1492 md5 = mi->referencesIdMD5();
1493 if ( !md5.isEmpty() )
1495 pParent = mThreadingCacheMessageIdMD5ToMessageItem.value( md5, 0 );
1496 if(pParent)
1498 // Take care of circular references
1499 if (
1500 ( mi == pParent ) || // self referencing message
1502 ( mi->childItemCount() > 0 ) && // mi already has children, this is fast to determine
1503 pParent->hasAncestor( mi ) // pParent is in the mi's children tree
1507 kWarning() << "Circular reference loop detected in the message tree";
1508 mi->setThreadingStatus( MessageItem::NonThreadable );
1509 return 0; // broken message: throw it away
1511 mi->setThreadingStatus( MessageItem::ImperfectParentFound );
1512 return pParent; // got an imperfect parent for this message
1515 // got no imperfect parent
1516 bMessageWasThreadable = true; // but the message was threadable
1519 if ( mAggregation->threading() == Aggregation::PerfectAndReferences )
1521 mi->setThreadingStatus( bMessageWasThreadable ? MessageItem::ParentMissing : MessageItem::NonThreadable );
1522 return 0; // we're doing only perfect parent matches
1525 Q_ASSERT( mAggregation->threading() == Aggregation::PerfectReferencesAndSubject );
1527 // We are supposed to do subject based threading but we can't do it now.
1528 // This is because the subject based threading *may* be wrong and waste
1529 // time by creating circular references (that we'd need to detect and fix).
1530 // We first try the perfect and references based threading on all the messages
1531 // and then run subject based threading only on the remaining ones.
1533 mi->setThreadingStatus( ( bMessageWasThreadable || mi->subjectIsPrefixed() ) ? MessageItem::ParentMissing : MessageItem::NonThreadable );
1534 return 0;
1537 // Subject threading cache stuff
1539 #if 0
1540 // Debug helpers
1541 void dump_iterator_and_list( QList< MessageItem * >::Iterator &iter, QList< MessageItem * > *list )
1543 kDebug() << "Threading cache part dump" << endl;
1544 if ( iter == list->end() )
1545 kDebug() << "Iterator pointing to end of the list" << endl;
1546 else
1547 kDebug() << "Iterator pointing to " << *iter << " subject [" << (*iter)->subject() << "] date [" << (*iter)->date() << "]" << endl;
1549 for ( QList< MessageItem * >::Iterator it = list->begin(); it != list->end(); ++it )
1551 kDebug() << "List element " << *it << " subject [" << (*it)->subject() << "] date [" << (*it)->date() << "]" << endl;
1554 kDebug() << "End of threading cache part dump" << endl;
1557 void dump_list( QList< MessageItem * > *list )
1559 kDebug() << "Threading cache part dump" << endl;
1561 for ( QList< MessageItem * >::Iterator it = list->begin(); it != list->end(); ++it )
1563 kDebug() << "List element " << *it << " subject [" << (*it)->subject() << "] date [" << (*it)->date() << "]" << endl;
1566 kDebug() << "End of threading cache part dump" << endl;
1568 #endif // debug helpers
1570 // a helper class used in a qLowerBound() call below
1571 class MessageLessThanByDate
1573 public:
1574 inline bool operator()( const MessageItem * mi1, const MessageItem * mi2 ) const
1576 if ( mi1->date() < mi2->date() ) // likely
1577 return true;
1578 if ( mi1->date() > mi2->date() ) // likely
1579 return false;
1580 // dates are equal, compare by pointer
1581 return mi1 < mi2;
1585 void ModelPrivate::addMessageToSubjectBasedThreadingCache( MessageItem * mi )
1587 // Messages in this cache are sorted by date, and if dates are equal then they are sorted by pointer value.
1588 // Sorting by date is used to optimize the parent lookup in guessMessageParent() below.
1590 // WARNING: If the message date changes for some reason (like in the "update" step)
1591 // then the cache may become unsorted. For this reason the message about to
1592 // be changed must be first removed from the cache and then reinserted.
1594 // Lookup the list of messages with the same stripped subject
1595 QList< MessageItem * > * messagesWithTheSameStrippedSubject =
1596 mThreadingCacheMessageSubjectMD5ToMessageItem.value( mi->strippedSubjectMD5(), 0 );
1598 if ( !messagesWithTheSameStrippedSubject )
1600 // Not there yet: create it and append.
1601 messagesWithTheSameStrippedSubject = new QList< MessageItem * >();
1602 mThreadingCacheMessageSubjectMD5ToMessageItem.insert( mi->strippedSubjectMD5(), messagesWithTheSameStrippedSubject );
1603 messagesWithTheSameStrippedSubject->append( mi );
1604 return;
1607 // Found: assert that we have no duplicates in the cache.
1608 Q_ASSERT( !messagesWithTheSameStrippedSubject->contains( mi ) );
1610 // Ordered insert: first by date then by pointer value.
1611 QList< MessageItem * >::Iterator it = qLowerBound( messagesWithTheSameStrippedSubject->begin(), messagesWithTheSameStrippedSubject->end(), mi, MessageLessThanByDate() );
1612 messagesWithTheSameStrippedSubject->insert( it, mi );
1615 void ModelPrivate::removeMessageFromSubjectBasedThreadingCache( MessageItem * mi )
1617 // We assume that the caller knows what he is doing and the message is actually in the cache.
1618 // If the message isn't in the cache then we should be called at all.
1620 // The game is called "performance"
1622 // Grab the list of all the messages with the same stripped subject (all potential parents)
1623 QList< MessageItem * > * messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value( mi->strippedSubjectMD5(), 0 );
1625 // We assume that the message is there so the list must be non null.
1626 Q_ASSERT( messagesWithTheSameStrippedSubject );
1628 // The cache *MUST* be ordered first by date then by pointer value
1629 QList< MessageItem * >::Iterator it = qLowerBound( messagesWithTheSameStrippedSubject->begin(), messagesWithTheSameStrippedSubject->end(), mi, MessageLessThanByDate() );
1631 // The binary based search must have found a message
1632 Q_ASSERT( it != messagesWithTheSameStrippedSubject->end() );
1634 // and it must have found exactly the message requested
1635 Q_ASSERT( *it == mi );
1637 // Kill it
1638 messagesWithTheSameStrippedSubject->erase( it );
1640 // And kill the list if it was the last one
1641 if ( messagesWithTheSameStrippedSubject->isEmpty() )
1643 mThreadingCacheMessageSubjectMD5ToMessageItem.remove( mi->strippedSubjectMD5() );
1644 delete messagesWithTheSameStrippedSubject;
1648 MessageItem * ModelPrivate::guessMessageParent( MessageItem * mi )
1650 // This function implements subject based threading
1651 // It attempts to guess a thread parent for the item "mi"
1652 // which actually may already have a children subtree.
1654 // We have all the problems of findMessageParent() plus the fact that
1655 // we're actually guessing (and often we may be *wrong*).
1657 Q_ASSERT( mAggregation->threading() == Aggregation::PerfectReferencesAndSubject ); // caller must take care of this
1658 Q_ASSERT( mi->subjectIsPrefixed() ); // caller must take care of this
1659 Q_ASSERT( mi->threadingStatus() == MessageItem::ParentMissing );
1662 // Do subject based threading
1663 const QByteArray md5 = mi->strippedSubjectMD5();
1664 if ( !md5.isEmpty() )
1666 QList< MessageItem * > * messagesWithTheSameStrippedSubject =
1667 mThreadingCacheMessageSubjectMD5ToMessageItem.value( md5, 0 );
1669 if ( messagesWithTheSameStrippedSubject )
1671 Q_ASSERT( messagesWithTheSameStrippedSubject->count() > 0 );
1673 // Need to find the message with the maximum date lower than the one of this message
1675 time_t maxTime = (time_t)0;
1676 MessageItem * pParent = 0;
1678 // Here'we re really guessing so circular references are possible
1679 // even on perfectly valid trees. This is why we don't consider it
1680 // an error but just continue searching.
1682 // FIXME: This might be speed up with an initial binary search (?)
1683 // ANSWER: No. We can't rely on date order (as it can be updated on the fly...)
1685 for ( QList< MessageItem * >::Iterator it = messagesWithTheSameStrippedSubject->begin(); it != messagesWithTheSameStrippedSubject->end(); ++it )
1687 int delta = mi->date() - ( *it )->date();
1689 // We don't take into account messages with a delta smaller than 120.
1690 // Assuming that our date() values are correct (that is, they take into
1691 // account timezones etc..) then one usually needs more than 120 seconds
1692 // to answer to a message. Better safe than sorry.
1694 // This check also includes negative deltas so messages later than mi aren't considered
1696 if ( delta < 120 )
1697 break; // The list is ordered by date (ascending) so we can stop searching here
1699 // About the "magic" 3628899 value here comes a Till's comment from the original KMHeaders:
1701 // "Parents more than six weeks older than the message are not accepted. The reasoning being
1702 // that if a new message with the same subject turns up after such a long time, the chances
1703 // that it is still part of the same thread are slim. The value of six weeks is chosen as a
1704 // result of a poll conducted on kde-devel, so it's probably bogus. :)"
1706 if ( delta < 3628899 )
1708 // Compute the closest.
1709 if ( ( maxTime < ( *it )->date() ) )
1711 // This algorithm *can* be (and often is) wrong.
1712 // Take care of circular threading which is really possible at this level.
1713 // If mi contains (*it) inside its children subtree then we have
1714 // found such a circular threading problem.
1716 // Note that here we can't have *it == mi because of the delta >= 120 check above.
1718 if ( ( mi->childItemCount() == 0 ) || !( *it )->hasAncestor( mi ) )
1720 maxTime = ( *it )->date();
1721 pParent = ( *it );
1727 if ( pParent )
1729 mi->setThreadingStatus( MessageItem::ImperfectParentFound );
1730 return pParent; // got an imperfect parent for this message
1735 return 0;
1739 // A little template helper, hopefully inlineable.
1741 // Return true if the specified message item is in the wrong position
1742 // inside the specified parent and needs re-sorting. Return false otherwise.
1743 // Both parent and messageItem must not be null.
1745 // Checking if a message needs re-sorting instead of just re-sorting it
1746 // is very useful since re-sorting is an expensive operation.
1748 template< class ItemComparator > static bool messageItemNeedsReSorting( SortOrder::SortDirection messageSortDirection,
1749 ItemPrivate *parent, MessageItem *messageItem )
1751 if ( ( messageSortDirection == SortOrder::Ascending )
1752 || ( parent->mType == Item::Message ) )
1754 return parent->childItemNeedsReSorting< ItemComparator, true >( messageItem );
1756 return parent->childItemNeedsReSorting< ItemComparator, false >( messageItem );
1759 bool ModelPrivate::handleItemPropertyChanges( int propertyChangeMask, Item * parent, Item * item )
1761 // The facts:
1763 // - If dates changed:
1764 // - If we're sorting messages by min/max date then at each level the messages might need resorting.
1765 // - If the thread leader is the most recent message of a thread then the uppermost
1766 // message of the thread might need re-grouping.
1767 // - If the groups are sorted by min/max date then the group might need re-sorting too.
1769 // This function explicitly doesn't re-apply the filter when ActionItemStatus changes.
1770 // This is because filters must be re-applied due to a broader range of status variations:
1771 // this is done in viewItemJobStepInternalForJobPass1Update() instead (which is the only
1772 // place in that ActionItemStatus may be set).
1774 if( parent->type() == Item::InvisibleRoot )
1776 // item is either a message or a group attacched to the root.
1777 // It might need resorting.
1778 if ( item->type() == Item::GroupHeader )
1780 // item is a group header attacched to the root.
1781 if (
1783 // max date changed
1784 ( propertyChangeMask & MaxDateChanged ) &&
1785 // groups sorted by max date
1786 ( mSortOrder->groupSorting() == SortOrder::SortGroupsByDateTimeOfMostRecent )
1787 ) || (
1788 // date changed
1789 ( propertyChangeMask & DateChanged ) &&
1790 // groups sorted by date
1791 ( mSortOrder->groupSorting() == SortOrder::SortGroupsByDateTime )
1795 // This group might need re-sorting.
1797 // Groups are large container of messages so it's likely that
1798 // another message inserted will cause this group to be marked again.
1799 // So we wait until the end to do the grand final re-sorting: it will be done in Pass4.
1800 mGroupHeadersThatNeedUpdate.insert( static_cast< GroupHeaderItem * >( item ), static_cast< GroupHeaderItem * >( item ) );
1802 } else {
1803 // item is a message. It might need re-sorting.
1805 // Since sorting is an expensive operation, we first check if it's *really* needed.
1806 // Re-sorting will actually not change min/max dates at all and
1807 // will not climb up the parent's ancestor tree.
1809 switch ( mSortOrder->messageSorting() )
1811 case SortOrder::SortMessagesByDateTime:
1812 if ( propertyChangeMask & DateChanged ) // date changed
1814 if ( messageItemNeedsReSorting< ItemDateComparator >( mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >( item ) ) )
1815 attachMessageToParent( parent, static_cast< MessageItem * >( item ) );
1816 } // else date changed, but it doesn't match sorting order: no need to re-sort
1817 break;
1818 case SortOrder::SortMessagesByDateTimeOfMostRecent:
1819 if ( propertyChangeMask & MaxDateChanged ) // max date changed
1821 if ( messageItemNeedsReSorting< ItemMaxDateComparator >( mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >( item ) ) )
1822 attachMessageToParent( parent, static_cast< MessageItem * >( item ) );
1823 } // else max date changed, but it doesn't match sorting order: no need to re-sort
1824 break;
1825 case SortOrder::SortMessagesByActionItemStatus:
1826 if ( propertyChangeMask & ActionItemStatusChanged ) // todo status changed
1828 if ( messageItemNeedsReSorting< ItemActionItemStatusComparator >( mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >( item ) ) )
1829 attachMessageToParent( parent, static_cast< MessageItem * >( item ) );
1830 } // else to do status changed, but it doesn't match sorting order: no need to re-sort
1831 break;
1832 case SortOrder::SortMessagesByUnreadStatus:
1833 if ( propertyChangeMask & UnreadStatusChanged ) // new / unread status changed
1835 if ( messageItemNeedsReSorting< ItemUnreadStatusComparator >( mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >( item ) ) )
1836 attachMessageToParent( parent, static_cast< MessageItem * >( item ) );
1837 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort
1838 break;
1839 default:
1840 // this kind of message sorting isn't affected by the property changes: nothing to do.
1841 break;
1845 return false; // the invisible root isn't affected by any change.
1848 if ( parent->type() == Item::GroupHeader )
1850 // item is a message attacched to a GroupHeader.
1851 // It might need re-grouping or re-sorting (within the same group)
1853 // Check re-grouping here.
1854 if (
1856 // max date changed
1857 ( propertyChangeMask & MaxDateChanged ) &&
1858 // thread leader is most recent message
1859 ( mAggregation->threadLeader() == Aggregation::MostRecentMessage )
1860 ) || (
1861 // date changed
1862 ( propertyChangeMask & DateChanged ) &&
1863 // thread leader the topmost message
1864 ( mAggregation->threadLeader() == Aggregation::TopmostMessage )
1868 // Might really need re-grouping.
1869 // attachMessageToGroupHeader() will find the right group for this message
1870 // and if it's different than the current it will move it.
1871 attachMessageToGroupHeader( static_cast< MessageItem * >( item ) );
1872 // Re-grouping fixes the properties of the involved group headers
1873 // so at exit of attachMessageToGroupHeader() the parent can't be affected
1874 // by the change anymore.
1875 return false;
1878 // Re-grouping wasn't needed. Re-sorting might be.
1880 } // else item is a message attacched to another message and might need re-sorting only.
1882 // Check if message needs re-sorting.
1884 switch ( mSortOrder->messageSorting() )
1886 case SortOrder::SortMessagesByDateTime:
1887 if ( propertyChangeMask & DateChanged ) // date changed
1889 if ( messageItemNeedsReSorting< ItemDateComparator >( mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >( item ) ) )
1890 attachMessageToParent( parent, static_cast< MessageItem * >( item ) );
1891 } // else date changed, but it doesn't match sorting order: no need to re-sort
1892 break;
1893 case SortOrder::SortMessagesByDateTimeOfMostRecent:
1894 if ( propertyChangeMask & MaxDateChanged ) // max date changed
1896 if ( messageItemNeedsReSorting< ItemMaxDateComparator >( mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >( item ) ) )
1897 attachMessageToParent( parent, static_cast< MessageItem * >( item ) );
1898 } // else max date changed, but it doesn't match sorting order: no need to re-sort
1899 break;
1900 case SortOrder::SortMessagesByActionItemStatus:
1901 if ( propertyChangeMask & ActionItemStatusChanged ) // todo status changed
1903 if ( messageItemNeedsReSorting< ItemActionItemStatusComparator >( mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >( item ) ) )
1904 attachMessageToParent( parent, static_cast< MessageItem * >( item ) );
1905 } // else to do status changed, but it doesn't match sorting order: no need to re-sort
1906 break;
1907 case SortOrder::SortMessagesByUnreadStatus:
1908 if ( propertyChangeMask & UnreadStatusChanged ) // new / unread status changed
1910 if ( messageItemNeedsReSorting< ItemUnreadStatusComparator >( mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >( item ) ) )
1911 attachMessageToParent( parent, static_cast< MessageItem * >( item ) );
1912 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort
1913 break;
1914 default:
1915 // this kind of message sorting isn't affected by property changes: nothing to do.
1916 break;
1919 return true; // parent might be affected too.
1922 void ModelPrivate::messageDetachedUpdateParentProperties( Item *oldParent, MessageItem *mi )
1924 Q_ASSERT( oldParent );
1925 Q_ASSERT( mi );
1926 Q_ASSERT( oldParent != mRootItem );
1929 // oldParent might have its properties changed because of the child removal.
1930 // propagate the changes up.
1931 for(;;)
1933 // pParent is not the root item now. This is assured by how we enter this loop
1934 // and by the fact that handleItemPropertyChanges returns false when grandParent
1935 // is Item::InvisibleRoot. We could actually assert it here...
1937 // Check if its dates need an update.
1938 int propertyChangeMask;
1940 if ( ( mi->maxDate() == oldParent->maxDate() ) && oldParent->recomputeMaxDate() )
1941 propertyChangeMask = MaxDateChanged;
1942 else
1943 break; // from the for(;;) loop
1945 // One of the oldParent properties has changed for sure
1947 Item * grandParent = oldParent->parent();
1949 // If there is no grandParent then oldParent isn't attacched to the view.
1950 // Re-sorting / re-grouping isn't needed for sure.
1951 if ( !grandParent )
1952 break; // from the for(;;) loop
1954 // The following function will return true if grandParent may be affected by the change.
1955 // If the grandParent isn't affected, we stop climbing.
1956 if ( !handleItemPropertyChanges( propertyChangeMask, grandParent, oldParent ) )
1957 break; // from the for(;;) loop
1959 // Now we need to climb up one level and check again.
1960 oldParent = grandParent;
1961 } // for(;;) loop
1963 // If the last message was removed from a group header then this group will need an update
1964 // for sure. We will need to remove it (unless a message is attacched back to it)
1965 if ( oldParent->type() == Item::GroupHeader )
1967 if ( oldParent->childItemCount() == 0 )
1968 mGroupHeadersThatNeedUpdate.insert( static_cast< GroupHeaderItem * >( oldParent ), static_cast< GroupHeaderItem * >( oldParent ) );
1972 void ModelPrivate::propagateItemPropertiesToParent( Item * item )
1974 Item * pParent = item->parent();
1975 Q_ASSERT( pParent );
1976 Q_ASSERT( pParent != mRootItem );
1978 for(;;)
1980 // pParent is not the root item now. This is assured by how we enter this loop
1981 // and by the fact that handleItemPropertyChanges returns false when grandParent
1982 // is Item::InvisibleRoot. We could actually assert it here...
1984 // Check if its dates need an update.
1985 int propertyChangeMask;
1987 if ( item->maxDate() > pParent->maxDate() )
1989 pParent->setMaxDate( item->maxDate() );
1990 propertyChangeMask = MaxDateChanged;
1991 } else {
1992 // No parent dates have changed: no further work is needed. Stop climbing here.
1993 break; // from the for(;;) loop
1996 // One of the pParent properties has changed.
1998 Item * grandParent = pParent->parent();
2000 // If there is no grandParent then pParent isn't attacched to the view.
2001 // Re-sorting / re-grouping isn't needed for sure.
2002 if ( !grandParent )
2003 break; // from the for(;;) loop
2005 // The following function will return true if grandParent may be affected by the change.
2006 // If the grandParent isn't affected, we stop climbing.
2007 if ( !handleItemPropertyChanges( propertyChangeMask, grandParent, pParent ) )
2008 break; // from the for(;;) loop
2010 // Now we need to climb up one level and check again.
2011 pParent = grandParent;
2013 } // for(;;)
2017 void ModelPrivate::attachMessageToParent( Item *pParent, MessageItem *mi )
2019 Q_ASSERT( pParent );
2020 Q_ASSERT( mi );
2022 // This function may be called to do a simple "re-sort" of the item inside the parent.
2023 // In that case mi->parent() is equal to pParent.
2024 bool oldParentWasTheSame;
2026 if ( mi->parent() )
2028 Item * oldParent = mi->parent();
2030 // The item already had a parent and this means that we're moving it.
2031 oldParentWasTheSame = oldParent == pParent; // just re-sorting ?
2033 if ( mi->isViewable() ) // is actually
2035 // The message is actually attached to the viewable root
2037 // Unfortunately we need to hack the model/view architecture
2038 // since it's somewhat flawed in this. At the moment of writing
2039 // there is simply no way to atomically move a subtree.
2040 // We must detach, call beginRemoveRows()/endRemoveRows(),
2041 // save the expanded state, save the selection, save the current item,
2042 // save the view position (YES! As we are removing items the view
2043 // will hopelessly jump around so we're just FORCED to break
2044 // the isolation from the view)...
2045 // ...*then* reattach, restore the expanded state, restore the selection,
2046 // restore the current item, restore the view position and pray
2047 // that nothing will fail in the (rather complicated) process....
2049 // Yet more unfortunately, while saving the expanded state might stop
2050 // at a certain (unexpanded) point in the tree, saving the selection
2051 // is hopelessly recursive down to the bare leafs.
2053 // Furthermore the expansion of items is a common case while selection
2054 // in the subtree is rare, so saving it would be a huge cost with
2055 // a low revenue.
2057 // This is why we just let the selection screw up. I hereby refuse to call
2058 // yet another expensive recursive function here :D
2060 // The current item saving can be somewhat optimized doing it once for
2061 // a single job step...
2063 if (
2064 ( ( mi )->childItemCount() > 0 ) && // has children
2065 mModelForItemFunctions && // the UI is not actually disconnected
2066 mView->isExpanded( q->index( mi, 0 ) ) // is actually expanded
2068 saveExpandedStateOfSubtree( mi );
2071 // If the parent is viewable (so mi was viewable too) then the beginRemoveRows()
2072 // and endRemoveRows() functions of this model will be called too.
2073 oldParent->takeChildItem( mModelForItemFunctions, mi );
2075 if ( ( !oldParentWasTheSame ) && ( oldParent != mRootItem ) )
2076 messageDetachedUpdateParentProperties( oldParent, mi );
2078 } else {
2079 // The item had no parent yet.
2080 oldParentWasTheSame = false;
2083 // Take care of perfect / imperfect threading.
2084 // Items that are now perfectly threaded, but already have a different parent
2085 // might have been imperfectly threaded before. Remove them from the caches.
2086 // Items that are now imperfectly threaded must be added to the caches.
2088 // If we're just re-sorting the item inside the same parent then the threading
2089 // caches don't need to be updated (since they actually depend on the parent).
2091 if ( !oldParentWasTheSame )
2093 switch( mi->threadingStatus() )
2095 case MessageItem::PerfectParentFound:
2096 if ( !mi->inReplyToIdMD5().isEmpty() )
2097 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.remove( mi->inReplyToIdMD5(), mi );
2098 break;
2099 case MessageItem::ImperfectParentFound:
2100 case MessageItem::ParentMissing: // may be: temporary or just fallback assignment
2101 if ( !mi->inReplyToIdMD5().isEmpty() )
2103 if ( !mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains( mi->inReplyToIdMD5(), mi ) )
2104 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.insert( mi->inReplyToIdMD5(), mi );
2106 break;
2107 case MessageItem::NonThreadable: // this also happens when we do no threading at all
2108 // make gcc happy
2109 Q_ASSERT( !mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains( mi->inReplyToIdMD5(), mi ) );
2110 break;
2114 // Set the new parent
2115 mi->setParent( pParent );
2117 // Propagate watched and ignored status
2118 if (
2119 ( pParent->status().toQInt32() & mCachedWatchedOrIgnoredStatusBits ) && // unlikely
2120 ( pParent->type() == Item::Message ) // likely
2123 // the parent is either watched or ignored: propagate to the child
2124 if ( pParent->status().isWatched() )
2126 int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow( mi );
2127 mi->setStatus( Akonadi::MessageStatus::statusWatched() );
2128 mStorageModel->setMessageItemStatus( mi, row, Akonadi::MessageStatus::statusWatched() );
2129 } else if ( pParent->status().isIgnored() )
2131 int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow( mi );
2132 mi->setStatus( Akonadi::MessageStatus::statusIgnored() );
2133 mStorageModel->setMessageItemStatus( mi, row, Akonadi::MessageStatus::statusIgnored() );
2137 // And insert into its child list
2139 // If pParent is viewable then the insert/append functions will call this model's
2140 // beginInsertRows() and endInsertRows() functions. This is EXTREMELY
2141 // expensive and ugly but it's the only way with the Qt4 imposed Model/View method.
2142 // Dude... (citation from Lost, if it wasn't clear).
2144 // I'm using a macro since it does really improve readability.
2145 // I'm NOT using a helper function since gcc will refuse to inline some of
2146 // the calls because they make this function grow too much.
2147 #define INSERT_MESSAGE_WITH_COMPARATOR( _ItemComparator ) \
2148 if ( ( mSortOrder->messageSortDirection() == SortOrder::Ascending ) \
2149 || ( pParent->type() == Item::Message ) ) \
2151 pParent->d_ptr->insertChildItem< _ItemComparator, true >( mModelForItemFunctions, mi ); \
2153 else \
2155 pParent->d_ptr->insertChildItem< _ItemComparator, false >( mModelForItemFunctions, mi ); \
2158 // If pParent is viewable then the insertion call will also set the child state to viewable.
2159 // Since mi MAY have children, then this call may make them viewable.
2160 switch( mSortOrder->messageSorting() )
2162 case SortOrder::SortMessagesByDateTime:
2163 INSERT_MESSAGE_WITH_COMPARATOR( ItemDateComparator )
2164 break;
2165 case SortOrder::SortMessagesByDateTimeOfMostRecent:
2166 INSERT_MESSAGE_WITH_COMPARATOR( ItemMaxDateComparator )
2167 break;
2168 case SortOrder::SortMessagesBySize:
2169 INSERT_MESSAGE_WITH_COMPARATOR( ItemSizeComparator )
2170 break;
2171 case SortOrder::SortMessagesBySenderOrReceiver:
2172 INSERT_MESSAGE_WITH_COMPARATOR( ItemSenderOrReceiverComparator )
2173 break;
2174 case SortOrder::SortMessagesBySender:
2175 INSERT_MESSAGE_WITH_COMPARATOR( ItemSenderComparator )
2176 break;
2177 case SortOrder::SortMessagesByReceiver:
2178 INSERT_MESSAGE_WITH_COMPARATOR( ItemReceiverComparator )
2179 break;
2180 case SortOrder::SortMessagesBySubject:
2181 INSERT_MESSAGE_WITH_COMPARATOR( ItemSubjectComparator )
2182 break;
2183 case SortOrder::SortMessagesByActionItemStatus:
2184 INSERT_MESSAGE_WITH_COMPARATOR( ItemActionItemStatusComparator )
2185 break;
2186 case SortOrder::SortMessagesByUnreadStatus:
2187 INSERT_MESSAGE_WITH_COMPARATOR( ItemUnreadStatusComparator )
2188 break;
2189 case SortOrder::NoMessageSorting:
2190 pParent->appendChildItem( mModelForItemFunctions, mi );
2191 break;
2192 default: // should never happen
2193 pParent->appendChildItem( mModelForItemFunctions, mi );
2194 break;
2197 // Decide if we need to expand parents
2198 bool childNeedsExpanding = ( mi->initialExpandStatus() == Item::ExpandNeeded );
2200 if ( pParent->initialExpandStatus() == Item::NoExpandNeeded )
2202 switch( mAggregation->threadExpandPolicy() )
2204 case Aggregation::NeverExpandThreads:
2205 // just do nothing unless this child has children and is already marked for expansion
2206 if ( childNeedsExpanding )
2207 pParent->setInitialExpandStatus( Item::ExpandNeeded );
2208 break;
2209 case Aggregation::ExpandThreadsWithNewMessages: // No more new status. fall through to unread if it exists in config
2210 case Aggregation::ExpandThreadsWithUnreadMessages:
2211 // expand only if unread (or it has children marked for expansion)
2212 if ( childNeedsExpanding || !mi->status().isRead() )
2213 pParent->setInitialExpandStatus( Item::ExpandNeeded );
2214 break;
2215 case Aggregation::ExpandThreadsWithUnreadOrImportantMessages:
2216 // expand only if unread, important or todo (or it has children marked for expansion)
2217 // FIXME: Wouldn't it be nice to be able to test for bitmasks in MessageStatus ?
2218 if ( childNeedsExpanding || !mi->status().isRead() || mi->status().isImportant() || mi->status().isToAct() )
2219 pParent->setInitialExpandStatus( Item::ExpandNeeded );
2220 break;
2221 case Aggregation::AlwaysExpandThreads:
2222 // expand everything
2223 pParent->setInitialExpandStatus( Item::ExpandNeeded );
2224 break;
2225 default:
2226 // BUG
2227 break;
2229 } // else it's already marked for expansion or expansion has been already executed
2231 // expand parent first, if possible
2232 if ( pParent->initialExpandStatus() == Item::ExpandNeeded )
2234 // If UI is not disconnected and parent is viewable, go up and expand
2235 if ( mModelForItemFunctions && pParent->isViewable() )
2237 // Now expand parents as needed
2238 Item * parentToExpand = pParent;
2239 while ( parentToExpand )
2241 if ( parentToExpand == mRootItem )
2242 break; // no need to set it expanded
2243 // parentToExpand is surely viewable (because this item is)
2244 if ( parentToExpand->initialExpandStatus() == Item::ExpandExecuted )
2245 break;
2247 mView->expand( q->index( parentToExpand, 0 ) );
2249 parentToExpand->setInitialExpandStatus( Item::ExpandExecuted );
2250 parentToExpand = parentToExpand->parent();
2252 } else {
2253 // It isn't viewable or UI is disconnected: climb up marking only
2254 Item * parentToExpand = pParent->parent();
2255 while ( parentToExpand )
2257 if ( parentToExpand == mRootItem )
2258 break; // no need to set it expanded
2259 parentToExpand->setInitialExpandStatus( Item::ExpandNeeded );
2260 parentToExpand = parentToExpand->parent();
2265 if ( mi->isViewable() )
2267 // mi is now viewable
2269 // sync subtree expanded status
2270 if ( childNeedsExpanding )
2272 if ( mi->childItemCount() > 0 )
2273 if ( mModelForItemFunctions ) // the UI is not disconnected
2274 syncExpandedStateOfSubtree( mi ); // sync the real state in the view
2277 // apply the filter, if needed
2278 if ( mFilter )
2280 Q_ASSERT( mModelForItemFunctions ); // the UI must be NOT disconnected here
2282 // apply the filter to subtree
2283 if ( applyFilterToSubtree( mi, q->index( pParent, 0 ) ) )
2285 // mi matched, expand parents (unconditionally)
2286 mView->ensureDisplayedWithParentsExpanded( mi );
2291 // Now we need to propagate the property changes the upper levels.
2293 // If we have just inserted a message inside the root then no work needs to be done:
2294 // no grouping is in effect and the message is already in the right place.
2295 if ( pParent == mRootItem )
2296 return;
2298 // If we have just removed the item from this parent and re-inserted it
2299 // then this operation was a simple re-sort. The code above didn't update
2300 // the properties when removing the item so we don't actually need
2301 // to make the updates back.
2302 if ( oldParentWasTheSame )
2303 return;
2305 // FIXME: OPTIMIZE THIS: First propagate changes THEN syncExpandedStateOfSubtree()
2306 // and applyFilterToSubtree... (needs some thinking though).
2308 // Time to propagate up.
2309 propagateItemPropertiesToParent( mi );
2311 // Aaah.. we're done. Time for a thea ? :)
2314 // FIXME: ThreadItem ?
2316 // Foo Bar, Joe Thommason, Martin Rox ... Eddie Maiden <date of the thread>
2317 // Title <number of messages>, Last by xxx <inner status>
2319 // When messages are added, mark it as dirty only (?)
2321 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass5( ViewItemJob *job, const QTime &tStart )
2323 // In this pass we scan the group headers that are in mGroupHeadersThatNeedUpdate.
2324 // Empty groups get deleted while the other ones are re-sorted.
2325 int elapsed;
2327 int curIndex = job->currentIndex();
2329 QHash< GroupHeaderItem *, GroupHeaderItem * >::Iterator it = mGroupHeadersThatNeedUpdate.begin();
2331 while ( it != mGroupHeadersThatNeedUpdate.end() )
2333 if ( ( *it )->childItemCount() == 0 )
2335 // group with no children, kill it
2336 ( *it )->parent()->takeChildItem( mModelForItemFunctions, *it );
2337 mGroupHeaderItemHash.remove( ( *it )->label() );
2339 // If we were going to restore its position after the job step, well.. we can't do it anymore.
2340 if ( mCurrentItemToRestoreAfterViewItemJobStep == ( *it ) )
2341 mCurrentItemToRestoreAfterViewItemJobStep = 0;
2343 // bye bye
2344 delete *it;
2345 } else {
2346 // Group with children: probably needs re-sorting.
2348 // Re-sorting here is an expensive operation.
2349 // In fact groups have been put in the QHash above on the assumption
2350 // that re-sorting *might* be needed but no real (expensive) check
2351 // has been done yet. Also by sorting a single group we might actually
2352 // put the others in the right place.
2353 // So finally check if re-sorting is *really* needed.
2354 bool needsReSorting;
2356 // A macro really improves readability here.
2357 #define CHECK_IF_GROUP_NEEDS_RESORTING( _ItemDateComparator ) \
2358 switch ( mSortOrder->groupSortDirection() ) \
2360 case SortOrder::Ascending: \
2361 needsReSorting = ( *it )->parent()->d_ptr->childItemNeedsReSorting< _ItemDateComparator, true >( *it ); \
2362 break; \
2363 case SortOrder::Descending: \
2364 needsReSorting = ( *it )->parent()->d_ptr->childItemNeedsReSorting< _ItemDateComparator, false >( *it ); \
2365 break; \
2366 default: /* should never happen */ \
2367 needsReSorting = false; \
2368 break; \
2371 switch ( mSortOrder->groupSorting() )
2373 case SortOrder::SortGroupsByDateTime:
2374 CHECK_IF_GROUP_NEEDS_RESORTING( ItemDateComparator )
2375 break;
2376 case SortOrder::SortGroupsByDateTimeOfMostRecent:
2377 CHECK_IF_GROUP_NEEDS_RESORTING( ItemMaxDateComparator )
2378 break;
2379 case SortOrder::SortGroupsBySenderOrReceiver:
2380 CHECK_IF_GROUP_NEEDS_RESORTING( ItemSenderOrReceiverComparator )
2381 break;
2382 case SortOrder::SortGroupsBySender:
2383 CHECK_IF_GROUP_NEEDS_RESORTING( ItemSenderComparator )
2384 break;
2385 case SortOrder::SortGroupsByReceiver:
2386 CHECK_IF_GROUP_NEEDS_RESORTING( ItemReceiverComparator )
2387 break;
2388 case SortOrder::NoGroupSorting:
2389 needsReSorting = false;
2390 break;
2391 default:
2392 // Should never happen... just assume re-sorting is not needed
2393 needsReSorting = false;
2394 break;
2397 if ( needsReSorting )
2398 attachGroup( *it ); // it will first detach and then re-attach in the proper place
2401 mGroupHeadersThatNeedUpdate.erase( it );
2402 it = mGroupHeadersThatNeedUpdate.begin();
2404 curIndex++;
2406 // FIXME: In fact a single update is likely to manipulate
2407 // a subtree with a LOT of messages inside. If interactivity is favored
2408 // we should check the time really more often.
2409 if ( ( curIndex % mViewItemJobStepMessageCheckCount ) == 0 )
2411 elapsed = tStart.msecsTo( QTime::currentTime() );
2412 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
2414 if ( it != mGroupHeadersThatNeedUpdate.end() )
2416 job->setCurrentIndex( curIndex );
2417 return ViewItemJobInterrupted;
2424 return ViewItemJobCompleted;
2429 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass4( ViewItemJob *job, const QTime &tStart )
2431 // In this pass we scan mUnassignedMessageListForPass4 which now
2432 // contains both items with parents and items without parents.
2433 // We scan mUnassignedMessageList for messages without parent (the ones that haven't been
2434 // attacched to the viewable tree yet) and find a suitable group for them. Then we simply
2435 // clear mUnassignedMessageList.
2437 // We call this pass "Grouping"
2439 int elapsed;
2441 int curIndex = job->currentIndex();
2442 int endIndex = job->endIndex();
2444 while ( curIndex <= endIndex )
2446 MessageItem * mi = mUnassignedMessageListForPass4[curIndex];
2447 if ( !mi->parent() )
2449 // Unassigned item: thread leader, insert into the proper group.
2450 // Locate the group (or root if no grouping requested)
2451 attachMessageToGroupHeader( mi );
2452 } else {
2453 // A parent was already assigned in Pass3: we have nothing to do here
2455 curIndex++;
2457 // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate
2458 // a subtree with a LOT of messages inside. If interactivity is favored
2459 // we should check the time really more often.
2460 if ( ( curIndex % mViewItemJobStepMessageCheckCount ) == 0 )
2462 elapsed = tStart.msecsTo( QTime::currentTime() );
2463 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
2465 if ( curIndex <= endIndex )
2467 job->setCurrentIndex( curIndex );
2468 return ViewItemJobInterrupted;
2474 mUnassignedMessageListForPass4.clear();
2475 return ViewItemJobCompleted;
2478 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass3( ViewItemJob *job, const QTime &tStart )
2480 // In this pass we scan the mUnassignedMessageListForPass3 and try to do construct the threads
2481 // by using subject based threading. If subject based threading is not in effect then
2482 // this pass turns to a nearly-no-op: at the end of Pass2 we have swapped the lists
2483 // and mUnassignedMessageListForPass3 is actually empty.
2485 // We don't shrink the mUnassignedMessageListForPass3 for two reasons:
2486 // - It would mess up this chunked algorithm by shifting indexes
2487 // - mUnassignedMessageList is a QList which is basically an array. It's faster
2488 // to traverse an array of N entries than to remove K>0 entries one by one and
2489 // to traverse the remaining N-K entries.
2491 int elapsed;
2493 int curIndex = job->currentIndex();
2494 int endIndex = job->endIndex();
2496 while ( curIndex <= endIndex )
2498 // If we're here, then threading is requested for sure.
2499 MessageItem * mi = mUnassignedMessageListForPass3[curIndex];
2500 if ( ( !mi->parent() ) || ( mi->threadingStatus() == MessageItem::ParentMissing ) )
2502 // Parent is missing (either "physically" with the item being not attacched or "logically"
2503 // with the item being attacched to a group or directly to the root.
2504 if ( mi->subjectIsPrefixed() )
2506 // We can try to guess it
2507 MessageItem * mparent = guessMessageParent( mi );
2509 if ( mparent )
2511 // imperfect parent found
2512 if ( mi->isViewable() )
2514 // mi was already viewable, we're just trying to re-parent it better...
2515 attachMessageToParent( mparent, mi );
2516 if ( !mparent->isViewable() )
2518 // re-attach it immediately (so current item is not lost)
2519 MessageItem * topmost = mparent->topmostMessage();
2520 Q_ASSERT( !topmost->parent() ); // groups are always viewable!
2521 topmost->setThreadingStatus( MessageItem::ParentMissing );
2522 attachMessageToGroupHeader( topmost );
2524 } else {
2525 // mi wasn't viewable yet.. no need to attach parent
2526 attachMessageToParent( mparent, mi );
2528 // and we're done for now
2529 } else {
2530 // so parent not found, (threadingStatus() is either MessageItem::ParentMissing or MessageItem::NonThreadable)
2531 Q_ASSERT( ( mi->threadingStatus() == MessageItem::ParentMissing ) || ( mi->threadingStatus() == MessageItem::NonThreadable ) );
2532 mUnassignedMessageListForPass4.append( mi ); // this is ~O(1)
2533 // and wait for Pass4
2535 } else {
2536 // can't guess the parent as the subject isn't prefixed
2537 Q_ASSERT( ( mi->threadingStatus() == MessageItem::ParentMissing ) || ( mi->threadingStatus() == MessageItem::NonThreadable ) );
2538 mUnassignedMessageListForPass4.append( mi ); // this is ~O(1)
2539 // and wait for Pass4
2541 } else {
2542 // Has a parent: either perfect parent already found or non threadable.
2543 // Since we don't end here if mi has status of parent missing then mi must not have imperfect parent.
2544 Q_ASSERT( mi->threadingStatus() != MessageItem::ImperfectParentFound );
2545 Q_ASSERT( mi->isViewable() );
2548 curIndex++;
2550 // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate
2551 // a subtree with a LOT of messages inside. If interactivity is favored
2552 // we should check the time really more often.
2553 if ( ( curIndex % mViewItemJobStepMessageCheckCount ) == 0 )
2555 elapsed = tStart.msecsTo( QTime::currentTime() );
2556 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
2558 if ( curIndex <= endIndex )
2560 job->setCurrentIndex( curIndex );
2561 return ViewItemJobInterrupted;
2567 mUnassignedMessageListForPass3.clear();
2568 return ViewItemJobCompleted;
2571 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass2( ViewItemJob *job, const QTime &tStart )
2573 // In this pass we scan the mUnassignedMessageList and try to do construct the threads.
2574 // If some thread leader message got attacched to the viewable tree in Pass1Fill then
2575 // we'll also attach all of its children too. The thread leaders we were unable
2576 // to attach in Pass1Fill and their children (which we find here) will make it to the small Pass3
2578 // We don't shrink the mUnassignedMessageList for two reasons:
2579 // - It would mess up this chunked algorithm by shifting indexes
2580 // - mUnassignedMessageList is a QList which is basically an array. It's faster
2581 // to traverse an array of N entries than to remove K>0 entries one by one and
2582 // to traverse the remaining N-K entries.
2584 // We call this pass "Threading"
2586 int elapsed;
2588 int curIndex = job->currentIndex();
2589 int endIndex = job->endIndex();
2591 while ( curIndex <= endIndex )
2593 // If we're here, then threading is requested for sure.
2594 MessageItem * mi = mUnassignedMessageListForPass2[curIndex];
2595 // The item may or may not have a parent.
2596 // If it has no parent or it has a temporary one (mi->parent() && mi->threadingStatus() == MessageItem::ParentMissing)
2597 // then we attempt to (re-)thread it. Otherwise we just do nothing (the job has already been done by the previous steps).
2598 if ( ( !mi->parent() ) || ( mi->threadingStatus() == MessageItem::ParentMissing ) )
2600 MessageItem * mparent = findMessageParent( mi );
2602 if ( mparent )
2604 // parent found, either perfect or imperfect
2605 if ( mi->isViewable() )
2607 // mi was already viewable, we're just trying to re-parent it better...
2608 attachMessageToParent( mparent, mi );
2609 if ( !mparent->isViewable() )
2611 // re-attach it immediately (so current item is not lost)
2612 MessageItem * topmost = mparent->topmostMessage();
2613 Q_ASSERT( !topmost->parent() ); // groups are always viewable!
2614 topmost->setThreadingStatus( MessageItem::ParentMissing );
2615 attachMessageToGroupHeader( topmost );
2617 } else {
2618 // mi wasn't viewable yet.. no need to attach parent
2619 attachMessageToParent( mparent, mi );
2621 // and we're done for now
2622 } else {
2623 // so parent not found, (threadingStatus() is either MessageItem::ParentMissing or MessageItem::NonThreadable)
2624 switch( mi->threadingStatus() )
2626 case MessageItem::ParentMissing:
2627 if ( mAggregation->threading() == Aggregation::PerfectReferencesAndSubject )
2629 // parent missing but still can be found in Pass3
2630 mUnassignedMessageListForPass3.append( mi ); // this is ~O(1)
2631 } else {
2632 // We're not doing subject based threading: will never be threaded, go straight to Pass4
2633 mUnassignedMessageListForPass4.append( mi ); // this is ~O(1)
2635 break;
2636 case MessageItem::NonThreadable:
2637 // will never be threaded, go straight to Pass4
2638 mUnassignedMessageListForPass4.append( mi ); // this is ~O(1)
2639 break;
2640 default:
2641 // a bug for sure
2642 kWarning() << "ERROR: Invalid message threading status returned by findMessageParent()!";
2643 Q_ASSERT( false );
2644 break;
2647 } else {
2648 // Has a parent: either perfect parent already found or non threadable.
2649 // Since we don't end here if mi has status of parent missing then mi must not have imperfect parent.
2650 Q_ASSERT( mi->threadingStatus() != MessageItem::ImperfectParentFound );
2651 if ( !mi->isViewable() )
2653 kWarning() << "Non viewable message " << mi << " subject " << mi->subject().toUtf8().data();
2654 Q_ASSERT( mi->isViewable() );
2658 curIndex++;
2660 // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate
2661 // a subtree with a LOT of messages inside. If interactivity is favored
2662 // we should check the time really more often.
2663 if ( ( curIndex % mViewItemJobStepMessageCheckCount ) == 0 )
2665 elapsed = tStart.msecsTo( QTime::currentTime() );
2666 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
2668 if ( curIndex <= endIndex )
2670 job->setCurrentIndex( curIndex );
2671 return ViewItemJobInterrupted;
2677 mUnassignedMessageListForPass2.clear();
2678 return ViewItemJobCompleted;
2681 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Fill( ViewItemJob *job, const QTime &tStart )
2683 // In this pass we scan the a contiguous region of the underlying storage (that is
2684 // assumed to be FLAT) and create the corresponding MessageItem objects.
2685 // The deal is to show items to the user as soon as possible so in this pass we
2686 // *TRY* to attach them to the viewable tree (which is rooted on mRootItem).
2687 // Messages we're unable to attach for some reason (mainly due to threading) get appended
2688 // to mUnassignedMessageList and wait for Pass2.
2690 // We call this pass "Processing"
2692 int elapsed;
2694 // Should we use the receiver or the sender field for sorting ?
2695 bool bUseReceiver = mStorageModelContainsOutboundMessages;
2697 // The begin storage index of our work
2698 int curIndex = job->currentIndex();
2699 // The end storage index of our work.
2700 int endIndex = job->endIndex();
2702 MessageItem * mi = 0;
2704 while( curIndex <= endIndex )
2706 // Create the message item with no parent: we'll set it later
2707 if ( !mi )
2709 mi = new MessageItem();
2710 } else {
2711 // a MessageItem discarded by a previous iteration: reuse it.
2712 Q_ASSERT( mi->parent() == 0 );
2715 if ( !mStorageModel->initializeMessageItem( mi, curIndex, bUseReceiver ) )
2717 // ugh
2718 kWarning() << "Fill of the MessageItem at storage row index " << curIndex << " failed";
2719 curIndex++;
2720 continue;
2723 // If we're supposed to pre-select a specific message, check if it's this one.
2724 if ( mUniqueIdOfLastSelectedMessageInFolder != 0 )
2726 // Yes.. a pre-selection is pending
2727 if( mUniqueIdOfLastSelectedMessageInFolder == mi->uniqueId() )
2729 // Found, it's this one.
2730 // But actually it's not viewable (so not selectable). We must wait
2731 // until the end of the job to be 100% sure. So here we just translate
2732 // the unique id to a MessageItem pointer and wait.
2733 mLastSelectedMessageInFolder = mi;
2734 mUniqueIdOfLastSelectedMessageInFolder = 0; // already found, don't bother checking anymore
2738 // Update the newest/oldest message, since we might be supposed to select those later
2739 if ( !mOldestItem || mOldestItem->date() > mi->date() ) {
2740 mOldestItem = mi;
2742 if ( !mNewestItem || mNewestItem->date() < mi->date() ) {
2743 mNewestItem = mi;
2746 // Ok.. it passed the initial checks: we will not be discarding it.
2747 // Make this message item an invariant index to the underlying model storage.
2748 mInvariantRowMapper->createModelInvariantIndex( curIndex, mi );
2751 // Attempt to do threading as soon as possible (to display items to the user)
2752 if ( mAggregation->threading() != Aggregation::NoThreading )
2754 // Threading is requested
2756 // Fetch the data needed for proper threading
2757 // Add the item to the threading caches
2759 switch( mAggregation->threading() )
2761 case Aggregation::PerfectReferencesAndSubject:
2762 mStorageModel->fillMessageItemThreadingData( mi, curIndex, StorageModel::PerfectThreadingReferencesAndSubject );
2764 // We also need to build the subject-based threading cache
2765 addMessageToSubjectBasedThreadingCache( mi );
2766 break;
2767 case Aggregation::PerfectAndReferences:
2768 mStorageModel->fillMessageItemThreadingData( mi, curIndex, StorageModel::PerfectThreadingPlusReferences );
2769 break;
2770 default:
2771 mStorageModel->fillMessageItemThreadingData( mi, curIndex, StorageModel::PerfectThreadingOnly );
2772 break;
2775 // Perfect/References threading cache
2776 mThreadingCacheMessageIdMD5ToMessageItem.insert( mi->messageIdMD5(), mi );
2778 // Check if this item is a perfect parent for some imperfectly threaded
2779 // message (that is actually attacched to it, but not necessairly to the
2780 // viewable root). If it is, then remove the imperfect child from its
2781 // current parent rebuild the hierarchy on the fly.
2783 bool needsImmediateReAttach = false;
2785 if ( mThreadingCacheMessageInReplyToIdMD5ToMessageItem.count() > 0 ) // unlikely
2787 QList< MessageItem * > lImperfectlyThreaded = mThreadingCacheMessageInReplyToIdMD5ToMessageItem.values( mi->messageIdMD5() );
2788 if ( !lImperfectlyThreaded.isEmpty() )
2790 // must move all of the items in the perfect parent
2791 for ( QList< MessageItem * >::Iterator it = lImperfectlyThreaded.begin(); it != lImperfectlyThreaded.end(); ++it )
2793 Q_ASSERT( ( *it )->parent() );
2794 Q_ASSERT( ( *it )->parent() != mi );
2796 if ( !( ( (*it)->threadingStatus() == MessageItem::ImperfectParentFound ) ||
2797 ( (*it)->threadingStatus() == MessageItem::ParentMissing ) ) ) {
2798 kError() << "Got message " << (*it) << " with threading status" << (*it)->threadingStatus();
2799 Q_ASSERT_X( false, "ModelPrivate::viewItemJobStepInternalForJobPass1Fill", "Wrong threading status" );
2802 // If the item was already attached to the view then
2803 // re-attach it immediately. This will avoid a message
2804 // being displayed for a short while in the view and then
2805 // disappear until a perfect parent isn't found.
2806 if ( ( *it )->isViewable() )
2807 needsImmediateReAttach = true;
2809 ( *it )->setThreadingStatus( MessageItem::PerfectParentFound );
2810 attachMessageToParent( mi, *it );
2815 // FIXME: Might look by "References" too, here... (?)
2817 // Attempt to do threading with anything we already have in caches until now
2818 // Note that this is likely to work since thread-parent messages tend
2819 // to come before thread-children messages in the folders (simply because of
2820 // date of arrival).
2822 Item * pParent;
2824 // First of all try to find a "perfect parent", that is the message for that
2825 // we have the ID in the "In-Reply-To" field. This is actually done by using
2826 // MD5 caches of the message ids because of speed. Collisions are very unlikely.
2828 const QByteArray md5 = mi->inReplyToIdMD5();
2830 if ( !md5.isEmpty() )
2832 // Have an In-Reply-To field MD5.
2833 // In well behaved mailing lists 70% of the threadable messages get a parent here :)
2834 pParent = mThreadingCacheMessageIdMD5ToMessageItem.value( md5, 0 );
2836 if( pParent ) // very likely
2838 if ( pParent == mi )
2840 // Bad, bad message.. it has In-Reply-To equal to MessageId...
2841 // Will wait for Pass2 with References-Id only
2842 mUnassignedMessageListForPass2.append( mi );
2843 } else {
2844 // wow, got a perfect parent for this message!
2845 mi->setThreadingStatus( MessageItem::PerfectParentFound );
2846 attachMessageToParent( pParent, mi );
2847 // we're done with this message (also for Pass2)
2849 } else {
2850 // got no parent
2851 // will have to wait Pass2
2852 mUnassignedMessageListForPass2.append( mi );
2854 } else {
2855 // No In-Reply-To header.
2857 bool mightHaveOtherMeansForThreading;
2859 switch( mAggregation->threading() )
2861 case Aggregation::PerfectReferencesAndSubject:
2862 mightHaveOtherMeansForThreading = mi->subjectIsPrefixed() || !mi->referencesIdMD5().isEmpty();
2863 break;
2864 case Aggregation::PerfectAndReferences:
2865 mightHaveOtherMeansForThreading = !mi->referencesIdMD5().isEmpty();
2866 break;
2867 case Aggregation::PerfectOnly:
2868 mightHaveOtherMeansForThreading = false;
2869 break;
2870 default:
2871 // BUG: there shouldn't be other values (NoThreading is excluded in an upper branch)
2872 Q_ASSERT( false );
2873 mightHaveOtherMeansForThreading = false; // make gcc happy
2874 break;
2877 if ( mightHaveOtherMeansForThreading )
2879 // We might have other means for threading this message, wait until Pass2
2880 mUnassignedMessageListForPass2.append( mi );
2881 } else {
2882 // No other means for threading this message. This is either
2883 // a standalone message or a thread leader.
2884 // If there is no grouping in effect or thread leaders are just the "topmost"
2885 // messages then we might be done with this one.
2886 if (
2887 ( mAggregation->grouping() == Aggregation::NoGrouping ) ||
2888 ( mAggregation->threadLeader() == Aggregation::TopmostMessage )
2891 // We're done with this message: it will be surely either toplevel (no grouping in effect)
2892 // or a thread leader with a well defined group. Do it :)
2893 //kDebug() << "Setting message status from " << mi->threadingStatus() << " to non threadable (1) " << mi;
2894 mi->setThreadingStatus( MessageItem::NonThreadable );
2895 // Locate the parent group for this item
2896 attachMessageToGroupHeader( mi );
2897 // we're done with this message (also for Pass2)
2898 } else {
2899 // Threads belong to the most recent message in the thread. This means
2900 // that we have to wait until Pass2 or Pass3 to assign a group.
2901 mUnassignedMessageListForPass2.append( mi );
2906 if ( needsImmediateReAttach && !mi->isViewable() )
2908 // The item gathered previously viewable children. They must be immediately
2909 // re-shown. So this item must currently be attached to the view.
2910 // This is a temporary measure: it will be probably still moved.
2911 MessageItem * topmost = mi->topmostMessage();
2912 Q_ASSERT( topmost->threadingStatus() == MessageItem::ParentMissing );
2913 attachMessageToGroupHeader( topmost );
2916 } else {
2917 // else no threading requested: we don't even need Pass2
2918 // set not threadable status (even if it might be not true, but in this mode we don't care)
2919 //kDebug() << "Setting message status from " << mi->threadingStatus() << " to non threadable (2) " << mi;
2920 mi->setThreadingStatus( MessageItem::NonThreadable );
2921 // locate the parent group for this item
2922 if ( mAggregation->grouping() == Aggregation::NoGrouping )
2923 attachMessageToParent( mRootItem, mi ); // no groups requested, attach directly to root
2924 else
2925 attachMessageToGroupHeader( mi );
2926 // we're done with this message (also for Pass2)
2929 mi = 0; // this item was pushed somewhere, create a new one at next iteration
2930 curIndex++;
2932 if ( ( curIndex % mViewItemJobStepMessageCheckCount ) == 0 )
2934 elapsed = tStart.msecsTo( QTime::currentTime() );
2935 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
2937 if ( curIndex <= endIndex )
2939 job->setCurrentIndex( curIndex );
2940 if ( mi )
2941 delete mi;
2942 return ViewItemJobInterrupted;
2948 if ( mi )
2949 delete mi;
2950 return ViewItemJobCompleted;
2953 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Cleanup( ViewItemJob *job, const QTime &tStart )
2955 Q_ASSERT( mModelForItemFunctions ); // UI must be not disconnected here
2956 // In this pass we remove the MessageItem objects that are present in the job
2957 // and put their children in the unassigned message list.
2959 // Note that this list in fact contains MessageItem objects (we need dynamic_cast<>).
2960 QList< ModelInvariantIndex * > * invalidatedMessages = job->invariantIndexList();
2962 // We don't shrink the invalidatedMessages because it's basically an array.
2963 // It's faster to traverse an array of N entries than to remove K>0 entries
2964 // one by one and to traverse the remaining N-K entries.
2966 int elapsed;
2968 // The begin index of our work
2969 int curIndex = job->currentIndex();
2970 // The end index of our work.
2971 int endIndex = job->endIndex();
2973 if ( curIndex == job->startIndex() )
2974 Q_ASSERT( mOrphanChildrenHash.isEmpty() );
2976 while( curIndex <= endIndex )
2978 // Get the underlying storage message data...
2979 MessageItem * dyingMessage = dynamic_cast< MessageItem * >( invalidatedMessages->at( curIndex ) );
2980 // This MUST NOT be null (otherwise we have a bug somewhere in this file).
2981 Q_ASSERT( dyingMessage );
2983 // If we were going to pre-select this message but we were interrupted
2984 // *before* it was actually made viewable, we just clear the pre-selection pointer
2985 // and unique id (abort pre-selection).
2986 if ( dyingMessage == mLastSelectedMessageInFolder )
2988 mLastSelectedMessageInFolder = 0;
2989 mUniqueIdOfLastSelectedMessageInFolder = 0;
2992 // remove the message from any pending user job
2993 if ( mPersistentSetManager )
2995 mPersistentSetManager->removeMessageItemFromAllSets( dyingMessage );
2996 if ( mPersistentSetManager->setCount() < 1 )
2998 delete mPersistentSetManager;
2999 mPersistentSetManager = 0;
3003 if ( dyingMessage->parent() )
3005 // Handle saving the current selection: if this item was the current before the step
3006 // then zero it out. We have killed it and it's OK for the current item to change.
3008 if ( dyingMessage == mCurrentItemToRestoreAfterViewItemJobStep )
3010 Q_ASSERT( dyingMessage->isViewable() );
3011 // Try to select the item below the removed one as it helps in doing a "readon" of emails:
3012 // you read a message, decide to delete it and then go to the next.
3013 // Qt tends to select the message above the removed one instead (this is a hardcoded logic in
3014 // QItemSelectionModelPrivate::_q_rowsAboutToBeRemoved()).
3015 mCurrentItemToRestoreAfterViewItemJobStep = mView->messageItemAfter( dyingMessage, MessageTypeAny, false );
3017 if ( !mCurrentItemToRestoreAfterViewItemJobStep )
3019 // There is no item below. Try the item above.
3020 // We still do it better than qt which tends to find the *thread* above
3021 // instead of the item above.
3022 mCurrentItemToRestoreAfterViewItemJobStep = mView->messageItemBefore( dyingMessage, MessageTypeAny, false );
3025 Q_ASSERT( (!mCurrentItemToRestoreAfterViewItemJobStep) || mCurrentItemToRestoreAfterViewItemJobStep->isViewable() );
3028 if (
3029 dyingMessage->isViewable() &&
3030 ( ( dyingMessage )->childItemCount() > 0 ) && // has children
3031 mView->isExpanded( q->index( dyingMessage, 0 ) ) // is actually expanded
3033 saveExpandedStateOfSubtree( dyingMessage );
3035 Item * oldParent = dyingMessage->parent();
3036 oldParent->takeChildItem( q, dyingMessage );
3038 // FIXME: This can generate many message movements.. it would be nicer
3039 // to start from messages that are higher in the hierarchy so
3040 // we would need to move less stuff above.
3042 if ( oldParent != mRootItem )
3043 messageDetachedUpdateParentProperties( oldParent, dyingMessage );
3045 // We might have already removed its parent from the view, so it
3046 // might already be in the orphan child hash...
3047 if ( dyingMessage->threadingStatus() == MessageItem::ParentMissing )
3048 mOrphanChildrenHash.remove( dyingMessage ); // this can turn to a no-op (dyingMessage not present in fact)
3050 } else {
3051 // The dying message had no parent: this should happen only if it's already an orphan
3053 Q_ASSERT( dyingMessage->threadingStatus() == MessageItem::ParentMissing );
3054 Q_ASSERT( mOrphanChildrenHash.contains( dyingMessage ) );
3055 Q_ASSERT( dyingMessage != mCurrentItemToRestoreAfterViewItemJobStep );
3057 mOrphanChildrenHash.remove( dyingMessage );
3060 if ( mAggregation->threading() != Aggregation::NoThreading )
3062 // Threading is requested: remove the message from threading caches.
3064 // Remove from the cache of potential parent items
3065 mThreadingCacheMessageIdMD5ToMessageItem.remove( dyingMessage->messageIdMD5() );
3067 // If we also have a cache for subject-based threading then remove the message from there too
3068 if( mAggregation->threading() == Aggregation::PerfectReferencesAndSubject )
3069 removeMessageFromSubjectBasedThreadingCache( dyingMessage );
3071 // If this message wasn't perfectly parented then it might still be in another cache.
3072 switch( dyingMessage->threadingStatus() )
3074 case MessageItem::ImperfectParentFound:
3075 case MessageItem::ParentMissing:
3076 if ( !dyingMessage->inReplyToIdMD5().isEmpty() )
3077 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.remove( dyingMessage->inReplyToIdMD5() );
3078 break;
3079 default:
3080 Q_ASSERT( !mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains( dyingMessage->inReplyToIdMD5(), dyingMessage ) );
3081 // make gcc happy
3082 break;
3086 while ( Item * childItem = dyingMessage->firstChildItem() )
3088 MessageItem * childMessage = dynamic_cast< MessageItem * >( childItem );
3089 Q_ASSERT( childMessage );
3091 dyingMessage->takeChildItem( q, childMessage );
3093 if ( mAggregation->threading() != Aggregation::NoThreading )
3095 if ( childMessage->threadingStatus() == MessageItem::PerfectParentFound )
3097 // If the child message was perfectly parented then now it had
3098 // lost its perfect parent. Add to the cache of imperfectly parented.
3099 if ( !childMessage->inReplyToIdMD5().isEmpty() )
3101 Q_ASSERT( !mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains( childMessage->inReplyToIdMD5(), childMessage ) );
3102 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.insert( childMessage->inReplyToIdMD5(), childMessage );
3107 // Parent is gone
3108 childMessage->setThreadingStatus( MessageItem::ParentMissing );
3110 // If the child (or any message in its subtree) is going to be selected,
3111 // then we must immediately reattach it to a temporary group in order for the
3112 // selection to be preserved across multiple steps. Otherwise we could end
3113 // with the child-to-be-selected being non viewable at the end
3114 // of the view job step. Attach to a temporary group.
3115 if (
3116 // child is going to be re-selected
3117 ( childMessage == mCurrentItemToRestoreAfterViewItemJobStep ) ||
3119 // there is a message that is going to be re-selected
3120 mCurrentItemToRestoreAfterViewItemJobStep &&
3121 // that message is in the childMessage subtree
3122 mCurrentItemToRestoreAfterViewItemJobStep->hasAncestor( childMessage )
3126 attachMessageToGroupHeader( childMessage );
3128 Q_ASSERT( childMessage->isViewable() );
3131 mOrphanChildrenHash.insert( childMessage, childMessage );
3134 if ( mNewestItem == dyingMessage ) {
3135 mNewestItem = 0;
3137 if ( mOldestItem == dyingMessage ) {
3138 mOldestItem = 0;
3141 delete dyingMessage;
3143 curIndex++;
3145 // FIXME: Maybe we should check smaller steps here since the
3146 // code above can generate large message tree movements
3147 // for each single item we sweep in the invalidatedMessages list.
3148 if ( ( curIndex % mViewItemJobStepMessageCheckCount ) == 0 )
3150 elapsed = tStart.msecsTo( QTime::currentTime() );
3151 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3153 if ( curIndex <= endIndex )
3155 job->setCurrentIndex( curIndex );
3156 return ViewItemJobInterrupted;
3162 // We looped over the entire deleted message list.
3164 job->setCurrentIndex( endIndex + 1 );
3166 // A quick last cleaning pass: this is usually very fast so we don't have a real
3167 // Pass enumeration for it. We just include it as trailer of Pass1Cleanup to be executed
3168 // when job->currentIndex() > job->endIndex();
3170 // We move all the messages from the orphan child hash to the unassigned message
3171 // list and get them ready for the standard Pass2.
3173 QHash< MessageItem *, MessageItem * >::Iterator it = mOrphanChildrenHash.begin();
3175 curIndex = 0;
3177 while ( it != mOrphanChildrenHash.end() )
3179 mUnassignedMessageListForPass2.append( *it );
3181 mOrphanChildrenHash.erase( it );
3183 it = mOrphanChildrenHash.begin();
3185 // This is still interruptible
3187 curIndex++;
3189 // FIXME: We could take "larger" steps here
3190 if ( ( curIndex % mViewItemJobStepMessageCheckCount ) == 0 )
3192 elapsed = tStart.msecsTo( QTime::currentTime() );
3193 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3195 if ( it != mOrphanChildrenHash.end() )
3196 return ViewItemJobInterrupted;
3201 return ViewItemJobCompleted;
3205 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Update( ViewItemJob *job, const QTime &tStart )
3207 Q_ASSERT( mModelForItemFunctions ); // UI must be not disconnected here
3209 // In this pass we simply update the MessageItem objects that are present in the job.
3211 // Note that this list in fact contains MessageItem objects (we need dynamic_cast<>).
3212 QList< ModelInvariantIndex * > * messagesThatNeedUpdate = job->invariantIndexList();
3214 // We don't shrink the messagesThatNeedUpdate because it's basically an array.
3215 // It's faster to traverse an array of N entries than to remove K>0 entries
3216 // one by one and to traverse the remaining N-K entries.
3218 int elapsed;
3220 // The begin index of our work
3221 int curIndex = job->currentIndex();
3222 // The end index of our work.
3223 int endIndex = job->endIndex();
3225 while( curIndex <= endIndex )
3227 // Get the underlying storage message data...
3228 MessageItem * message = dynamic_cast< MessageItem * >( messagesThatNeedUpdate->at( curIndex ) );
3229 // This MUST NOT be null (otherwise we have a bug somewhere in this file).
3230 Q_ASSERT( message );
3232 int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow( message );
3234 if ( row < 0 )
3236 // Must have been invalidated (so it's basically about to be deleted)
3237 Q_ASSERT( !message->isValid() );
3238 // Skip it here.
3239 curIndex++;
3240 continue;
3243 time_t prevDate = message->date();
3244 time_t prevMaxDate = message->maxDate();
3245 bool toDoStatus = message->status().isToAct();
3246 bool prevUnreadStatus = !message->status().isRead();
3248 // The subject based threading cache is sorted by date: we must remove
3249 // the item and re-insert it since updateMessageItemData() may change the date too.
3250 if( mAggregation->threading() == Aggregation::PerfectReferencesAndSubject )
3251 removeMessageFromSubjectBasedThreadingCache( message );
3253 // Do update
3254 mStorageModel->updateMessageItemData( message, row );
3255 QModelIndex idx = q->index( message, 0 );
3256 emit q->dataChanged( idx, idx );
3258 // Reinsert the item to the cache, if needed
3259 if( mAggregation->threading() == Aggregation::PerfectReferencesAndSubject )
3260 addMessageToSubjectBasedThreadingCache( message );
3263 int propertyChangeMask = 0;
3265 if ( prevDate != message->date() )
3266 propertyChangeMask |= DateChanged;
3267 if ( prevMaxDate != message->maxDate() )
3268 propertyChangeMask |= MaxDateChanged;
3269 if ( toDoStatus != message->status().isToAct() )
3270 propertyChangeMask |= ActionItemStatusChanged;
3271 if ( prevUnreadStatus != ( !message->status().isRead() ) )
3272 propertyChangeMask |= UnreadStatusChanged;
3274 if ( propertyChangeMask )
3276 // Some message data has changed
3277 // now we need to handle the changes that might cause re-grouping/re-sorting
3278 // and propagate them to the parents.
3280 Item * pParent = message->parent();
3282 if ( pParent && ( pParent != mRootItem ) )
3284 // The following function will return true if itemParent may be affected by the change.
3285 // If the itemParent isn't affected, we stop climbing.
3286 if ( handleItemPropertyChanges( propertyChangeMask, pParent, message ) )
3288 Q_ASSERT( message->parent() ); // handleItemPropertyChanges() must never leave an item detached
3290 // Note that actually message->parent() may be different than pParent since
3291 // handleItemPropertyChanges() may have re-grouped it.
3293 // Time to propagate up.
3294 propagateItemPropertiesToParent( message );
3296 } // else there is no parent so the item isn't attached to the view: re-grouping/re-sorting not needed.
3297 } // else message data didn't change an there is nothing interesting to do
3299 // (re-)apply the filter, if needed
3300 if ( mFilter && message->isViewable() )
3302 // In all the other cases we (re-)apply the filter to the topmost subtree that this message is in.
3303 Item * pTopMostNonRoot = message->topmostNonRoot();
3305 Q_ASSERT( pTopMostNonRoot );
3306 Q_ASSERT( pTopMostNonRoot != mRootItem );
3307 Q_ASSERT( pTopMostNonRoot->parent() == mRootItem );
3309 // FIXME: The call below works, but it's expensive when we are updating
3310 // a lot of items with filtering enabled. This is because the updated
3311 // items are likely to be in the same subtree which we then filter multiple times.
3312 // A point for us is that when filtering there shouldn't be really many
3313 // items in the view so the user isn't going to update a lot of them at once...
3314 // Well... anyway, the alternative would be to write yet another
3315 // specialized routine that would update only the "message" item
3316 // above and climb up eventually hiding parents (without descending the sibling subtrees again).
3317 // If people complain about performance in this particular case I'll consider that solution.
3319 applyFilterToSubtree( pTopMostNonRoot, QModelIndex() );
3321 } // otherwise there is no filter or the item isn't viewable: very likely
3322 // left detached while propagating property changes. Will filter it
3323 // on reattach.
3325 // Done updating this message
3327 curIndex++;
3329 // FIXME: Maybe we should check smaller steps here since the
3330 // code above can generate large message tree movements
3331 // for each single item we sweep in the messagesThatNeedUpdate list.
3332 if ( ( curIndex % mViewItemJobStepMessageCheckCount ) == 0 )
3334 elapsed = tStart.msecsTo( QTime::currentTime() );
3335 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3337 if ( curIndex <= endIndex )
3339 job->setCurrentIndex( curIndex );
3340 return ViewItemJobInterrupted;
3346 return ViewItemJobCompleted;
3350 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJob( ViewItemJob *job, const QTime &tStart )
3352 // This function does a timed chunk of work for a single Fill View job.
3353 // It attempts to process messages until a timeout forces it to return to the caller.
3355 // A macro would improve readability here but since this is a good point
3356 // to place debugger breakpoints then we need it explicited.
3357 // A (template) helper would need to pass many parameters and would not be inlined...
3359 int elapsed;
3361 if ( job->currentPass() == ViewItemJob::Pass1Fill )
3363 // We're in Pass1Fill of the job.
3364 switch ( viewItemJobStepInternalForJobPass1Fill( job, tStart ) )
3366 case ViewItemJobInterrupted:
3367 // current job interrupted by timeout: propagate status to caller
3368 return ViewItemJobInterrupted;
3369 break;
3370 case ViewItemJobCompleted:
3371 // pass 1 has been completed
3372 // # TODO: Refactor this, make it virtual or whatever, but switch == bad, code duplication etc
3373 job->setCurrentPass( ViewItemJob::Pass2 );
3374 job->setStartIndex( 0 );
3375 job->setEndIndex( mUnassignedMessageListForPass2.count() - 1 );
3376 // take care of small jobs which never timeout by themselves because
3377 // of a small number of messages. At the end of each job check
3378 // the time used and if we're timeoutting and there is another job
3379 // then interrupt.
3380 elapsed = tStart.msecsTo( QTime::currentTime() );
3381 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3383 return ViewItemJobInterrupted;
3384 } // else proceed with the next pass
3385 break;
3386 default:
3387 // This is *really* a BUG
3388 kWarning() << "ERROR: returned an invalid result";
3389 Q_ASSERT( false );
3390 break;
3392 } else if ( job->currentPass() == ViewItemJob::Pass1Cleanup )
3394 // We're in Pass1Cleanup of the job.
3395 switch ( viewItemJobStepInternalForJobPass1Cleanup( job, tStart ) )
3397 case ViewItemJobInterrupted:
3398 // current job interrupted by timeout: propagate status to caller
3399 return ViewItemJobInterrupted;
3400 break;
3401 case ViewItemJobCompleted:
3402 // pass 1 has been completed
3403 job->setCurrentPass( ViewItemJob::Pass2 );
3404 job->setStartIndex( 0 );
3405 job->setEndIndex( mUnassignedMessageListForPass2.count() - 1 );
3406 // take care of small jobs which never timeout by themselves because
3407 // of a small number of messages. At the end of each job check
3408 // the time used and if we're timeoutting and there is another job
3409 // then interrupt.
3410 elapsed = tStart.msecsTo( QTime::currentTime() );
3411 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3413 return ViewItemJobInterrupted;
3414 } // else proceed with the next pass
3415 break;
3416 default:
3417 // This is *really* a BUG
3418 kWarning() << "ERROR: returned an invalid result";
3419 Q_ASSERT( false );
3420 break;
3422 } else if ( job->currentPass() == ViewItemJob::Pass1Update )
3424 // We're in Pass1Update of the job.
3425 switch ( viewItemJobStepInternalForJobPass1Update( job, tStart ) )
3427 case ViewItemJobInterrupted:
3428 // current job interrupted by timeout: propagate status to caller
3429 return ViewItemJobInterrupted;
3430 break;
3431 case ViewItemJobCompleted:
3432 // pass 1 has been completed
3433 // Since Pass2, Pass3 and Pass4 are empty for an Update operation
3434 // we simply skip them. (TODO: Triple-verify this assertion...).
3435 job->setCurrentPass( ViewItemJob::Pass5 );
3436 job->setStartIndex( 0 );
3437 job->setEndIndex( mGroupHeadersThatNeedUpdate.count() - 1 );
3438 // take care of small jobs which never timeout by themselves because
3439 // of a small number of messages. At the end of each job check
3440 // the time used and if we're timeoutting and there is another job
3441 // then interrupt.
3442 elapsed = tStart.msecsTo( QTime::currentTime() );
3443 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3445 return ViewItemJobInterrupted;
3446 } // else proceed with the next pass
3447 break;
3448 default:
3449 // This is *really* a BUG
3450 kWarning() << "ERROR: returned an invalid result";
3451 Q_ASSERT( false );
3452 break;
3456 // Pass1Fill/Pass1Cleanup/Pass1Update has been already completed.
3458 if ( job->currentPass() == ViewItemJob::Pass2 )
3460 // We're in Pass2 of the job.
3461 switch ( viewItemJobStepInternalForJobPass2( job, tStart ) )
3463 case ViewItemJobInterrupted:
3464 // current job interrupted by timeout: propagate status to caller
3465 return ViewItemJobInterrupted;
3466 break;
3467 case ViewItemJobCompleted:
3468 // pass 2 has been completed
3469 job->setCurrentPass( ViewItemJob::Pass3 );
3470 job->setStartIndex( 0 );
3471 job->setEndIndex( mUnassignedMessageListForPass3.count() - 1 );
3472 // take care of small jobs which never timeout by themselves because
3473 // of a small number of messages. At the end of each job check
3474 // the time used and if we're timeoutting and there is another job
3475 // then interrupt.
3476 elapsed = tStart.msecsTo( QTime::currentTime() );
3477 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3478 return ViewItemJobInterrupted;
3479 // else proceed with the next pass
3480 break;
3481 default:
3482 // This is *really* a BUG
3483 kWarning() << "ERROR: returned an invalid result";
3484 Q_ASSERT( false );
3485 break;
3489 if ( job->currentPass() == ViewItemJob::Pass3 )
3491 // We're in Pass3 of the job.
3492 switch ( viewItemJobStepInternalForJobPass3( job, tStart ) )
3494 case ViewItemJobInterrupted:
3495 // current job interrupted by timeout: propagate status to caller
3496 return ViewItemJobInterrupted;
3497 break;
3498 case ViewItemJobCompleted:
3499 // pass 3 has been completed
3500 job->setCurrentPass( ViewItemJob::Pass4 );
3501 job->setStartIndex( 0 );
3502 job->setEndIndex( mUnassignedMessageListForPass4.count() - 1 );
3503 // take care of small jobs which never timeout by themselves because
3504 // of a small number of messages. At the end of each job check
3505 // the time used and if we're timeoutting and there is another job
3506 // then interrupt.
3507 elapsed = tStart.msecsTo( QTime::currentTime() );
3508 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3509 return ViewItemJobInterrupted;
3510 // else proceed with the next pass
3511 break;
3512 default:
3513 // This is *really* a BUG
3514 kWarning() << "ERROR: returned an invalid result";
3515 Q_ASSERT( false );
3516 break;
3520 if ( job->currentPass() == ViewItemJob::Pass4 )
3522 // We're in Pass4 of the job.
3523 switch ( viewItemJobStepInternalForJobPass4( job, tStart ) )
3525 case ViewItemJobInterrupted:
3526 // current job interrupted by timeout: propagate status to caller
3527 return ViewItemJobInterrupted;
3528 break;
3529 case ViewItemJobCompleted:
3530 // pass 4 has been completed
3531 job->setCurrentPass( ViewItemJob::Pass5 );
3532 job->setStartIndex( 0 );
3533 job->setEndIndex( mGroupHeadersThatNeedUpdate.count() - 1 );
3534 // take care of small jobs which never timeout by themselves because
3535 // of a small number of messages. At the end of each job check
3536 // the time used and if we're timeoutting and there is another job
3537 // then interrupt.
3538 elapsed = tStart.msecsTo( QTime::currentTime() );
3539 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3540 return ViewItemJobInterrupted;
3541 // else proceed with the next pass
3542 break;
3543 default:
3544 // This is *really* a BUG
3545 kWarning() << "ERROR: returned an invalid result";;
3546 Q_ASSERT( false );
3547 break;
3551 // Pass4 has been already completed. Proceed to Pass5.
3552 return viewItemJobStepInternalForJobPass5( job, tStart );
3555 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3557 // Namespace to collect all the vars and functions for KDEPIM_FOLDEROPEN_PROFILE
3558 namespace Stats {
3560 // Number of existing jobs/passes
3561 static const int numberOfPasses = ViewItemJob::LastIndex;
3563 // The pass in the last call of viewItemJobStepInternal(), used to detect when
3564 // a new pass starts
3565 static int lastPass = -1;
3567 // Total number of messages in the folder
3568 static int totalMessages;
3570 // Per-Job data
3571 static int numElements[numberOfPasses];
3572 static int totalTime[numberOfPasses];
3573 static int chunks[numberOfPasses];
3575 // Time, in msecs for some special operations
3576 static int expandingTreeTime;
3577 static int layoutChangeTime;
3579 // Descriptions of the job, for nicer debug output
3580 static const char *jobDescription[numberOfPasses] = {
3581 "Creating items from messages and simple threading",
3582 "Removing messages",
3583 "Updating messages",
3584 "Additional Threading",
3585 "Subject-Based threading",
3586 "Grouping",
3587 "Group resorting + cleanup"
3590 // Timer to track time between start of first job and end of last job
3591 static QTime firstStartTime;
3593 // Timer to track time the current job takes
3594 static QTime currentJobStartTime;
3596 // Zeros the stats, to be called when the first job starts
3597 static void resetStats()
3599 totalMessages = 0;
3600 layoutChangeTime = 0;
3601 expandingTreeTime = 0;
3602 lastPass = -1;
3603 for ( int i = 0; i < numberOfPasses; i++ ) {
3604 numElements[i] = 0;
3605 totalTime[i] = 0;
3606 chunks[i] = 0;
3610 } // namespace Stats
3612 void ModelPrivate::printStatistics()
3614 using namespace Stats;
3615 int totalTotalTime = 0;
3616 int completeTime = firstStartTime.elapsed();
3617 for ( int i = 0; i < numberOfPasses; i++ )
3618 totalTotalTime += totalTime[i];
3620 float msgPerSecond = totalMessages / ( totalTotalTime / 1000.0f );
3621 float msgPerSecondComplete = totalMessages / ( completeTime / 1000.0f );
3623 int messagesWithSameSubjectAvg = 0;
3624 int messagesWithSameSubjectMax = 0;
3625 foreach( const QList< MessageItem * > *messages, mThreadingCacheMessageSubjectMD5ToMessageItem ) {
3626 if ( messages->size() > messagesWithSameSubjectMax )
3627 messagesWithSameSubjectMax = messages->size();
3628 messagesWithSameSubjectAvg += messages->size();
3630 messagesWithSameSubjectAvg = messagesWithSameSubjectAvg / (float)mThreadingCacheMessageSubjectMD5ToMessageItem.size();
3632 int totalThreads = 0;
3633 if ( !mGroupHeaderItemHash.isEmpty() ) {
3634 foreach( const GroupHeaderItem *groupHeader, mGroupHeaderItemHash ) {
3635 totalThreads += groupHeader->childItemCount();
3638 else
3639 totalThreads = mRootItem->childItemCount();
3641 kDebug() << "Finished filling the view with" << totalMessages << "messages";
3642 kDebug() << "That took" << totalTotalTime << "msecs inside the model and"
3643 << completeTime << "in total.";
3644 kDebug() << ( totalTotalTime / (float) completeTime ) * 100.0f
3645 << "percent of the time was spent in the model.";
3646 kDebug() << "Time for layoutChanged(), in msecs:" << layoutChangeTime
3647 << "(" << (layoutChangeTime / (float)totalTotalTime) * 100.0f << "percent )";
3648 kDebug() << "Time to expand tree, in msecs:" << expandingTreeTime
3649 << "(" << (expandingTreeTime / (float)totalTotalTime) * 100.0f << "percent )";
3650 kDebug() << "Number of messages per second in the model:" << msgPerSecond;
3651 kDebug() << "Number of messages per second in total:" << msgPerSecondComplete;
3652 kDebug() << "Number of threads:" << totalThreads;
3653 kDebug() << "Number of groups:" << mGroupHeaderItemHash.size();
3654 kDebug() << "Messages per thread:" << totalMessages / (float)totalThreads;
3655 kDebug() << "Threads per group:" << totalThreads / (float)mGroupHeaderItemHash.size();
3656 kDebug() << "Messages with the same subject:"
3657 << "Max:" << messagesWithSameSubjectMax
3658 << "Avg:" << messagesWithSameSubjectAvg;
3659 kDebug();
3660 kDebug() << "Now follows a breakdown of the jobs.";
3661 kDebug();
3662 for ( int i = 0; i < numberOfPasses; i++ ) {
3663 if ( totalTime[i] == 0 )
3664 continue;
3665 float elementsPerSecond = numElements[i] / ( totalTime[i] / 1000.0f );
3666 float percent = totalTime[i] / (float)totalTotalTime * 100.0f;
3667 kDebug() << "----------------------------------------------";
3668 kDebug() << "Job" << i + 1 << "(" << jobDescription[i] << ")";
3669 kDebug() << "Share of complete time:" << percent << "percent";
3670 kDebug() << "Time in msecs:" << totalTime[i];
3671 kDebug() << "Number of elements:" << numElements[i]; // TODO: map of element string
3672 kDebug() << "Elements per second:" << elementsPerSecond;
3673 kDebug() << "Number of chunks:" << chunks[i];
3674 kDebug();
3677 kDebug() << "==========================================================";
3678 resetStats();
3681 #endif
3683 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternal()
3685 // This function does a timed chunk of work in our View Fill operation.
3686 // It attempts to do processing until it either runs out of jobs
3687 // to be done or a timeout forces it to interrupt and jump back to the caller.
3689 QTime tStart = QTime::currentTime();
3690 int elapsed;
3692 while( !mViewItemJobs.isEmpty() )
3694 // Have a job to do.
3695 ViewItemJob * job = mViewItemJobs.first();
3697 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3699 // Here we check if an old job has just completed or if we are at the start of the
3700 // first job. We then initialize job data stuff and timers based on this.
3702 const int currentPass = job->currentPass();
3703 const bool firstChunk = currentPass != Stats::lastPass;
3704 if ( currentPass != Stats::lastPass && Stats::lastPass != -1 ) {
3705 Stats::totalTime[Stats::lastPass] = Stats::currentJobStartTime.elapsed();
3707 const bool firstJob = job->currentPass() == ViewItemJob::Pass1Fill && firstChunk;
3708 const int elements = job->endIndex() - job->startIndex();
3709 if ( firstJob ) {
3710 Stats::resetStats();
3711 Stats::totalMessages = elements;
3712 Stats::firstStartTime.restart();
3714 if ( firstChunk ) {
3715 Stats::numElements[currentPass] = elements;
3716 Stats::currentJobStartTime.restart();
3718 Stats::chunks[currentPass]++;
3719 Stats::lastPass = currentPass;
3721 #endif
3723 mViewItemJobStepIdleInterval = job->idleInterval();
3724 mViewItemJobStepChunkTimeout = job->chunkTimeout();
3725 mViewItemJobStepMessageCheckCount = job->messageCheckCount();
3727 if ( job->disconnectUI() )
3729 mModelForItemFunctions = 0; // disconnect the UI for this job
3730 Q_ASSERT( mLoading ); // this must be true in the first job
3731 // FIXME: Should assert yet more that this is the very first job for this StorageModel
3732 // Asserting only mLoading is not enough as we could be using a two-jobs loading strategy
3733 // or this could be a job enqueued before the first job has completed.
3734 } else {
3735 // With a connected UI we need to avoid the view to update the scrollbars at EVERY insertion or expansion.
3736 // QTreeViewPrivate::updateScrollBars() is very expensive as it loops through ALL the items in the view every time.
3737 // We can't disable the function directly as it's hidden in the private data object of QTreeView
3738 // but we can disable the parent QTreeView::updateGeometries() instead.
3739 // We will trigger it "manually" at the end of the step.
3740 mView->ignoreUpdateGeometries( true );
3742 // Ok.. I know that this seems unbelieveable but disabling updates actually
3743 // causes a (significant) performance loss in most cases. This is probably because QTreeView
3744 // uses delayed layouts when updates are disabled which should be delayed but in
3745 // fact are "forced" by next item insertions. The delayed layout algorithm, then
3746 // is probably slower than the non-delayed one.
3747 // Disabling the paintEvent() doesn't seem to work either.
3748 //mView->setUpdatesEnabled( false );
3751 switch( viewItemJobStepInternalForJob( job, tStart ) )
3753 case ViewItemJobInterrupted:
3755 // current job interrupted by timeout: will propagate status to caller
3756 // but before this, give some feedback to the user
3758 // FIXME: This is now inaccurate, think of something else
3759 switch( job->currentPass() )
3761 case ViewItemJob::Pass1Fill:
3762 case ViewItemJob::Pass1Cleanup:
3763 case ViewItemJob::Pass1Update:
3764 emit q->statusMessage( i18np( "Processed 1 Message of %2",
3765 "Processed %1 Messages of %2",
3766 job->currentIndex() - job->startIndex(),
3767 job->endIndex() - job->startIndex() + 1 ) );
3768 break;
3769 case ViewItemJob::Pass2:
3770 emit q->statusMessage( i18np( "Threaded 1 Message of %2",
3771 "Threaded %1 Messages of %2",
3772 job->currentIndex() - job->startIndex(),
3773 job->endIndex() - job->startIndex() + 1 ) );
3774 break;
3775 case ViewItemJob::Pass3:
3776 emit q->statusMessage( i18np( "Threaded 1 Message of %2",
3777 "Threaded %1 Messages of %2",
3778 job->currentIndex() - job->startIndex(),
3779 job->endIndex() - job->startIndex() + 1 ) );
3780 break;
3781 case ViewItemJob::Pass4:
3782 emit q->statusMessage( i18np( "Grouped 1 Thread of %2",
3783 "Grouped %1 Threads of %2",
3784 job->currentIndex() - job->startIndex(),
3785 job->endIndex() - job->startIndex() + 1 ) );
3786 break;
3787 case ViewItemJob::Pass5:
3788 emit q->statusMessage( i18np( "Updated 1 Group of %2",
3789 "Updated %1 Groups of %2",
3790 job->currentIndex() - job->startIndex(),
3791 job->endIndex() - job->startIndex() + 1 ) );
3792 break;
3793 default: break;
3796 if( !job->disconnectUI() )
3798 mView->ignoreUpdateGeometries( false );
3799 // explicit call to updateGeometries() here
3800 mView->updateGeometries();
3803 return ViewItemJobInterrupted;
3805 break;
3806 case ViewItemJobCompleted:
3808 // If this job worked with a disconnected UI, emit layoutChanged()
3809 // to reconnect it. We go back to normal operation now.
3810 if ( job->disconnectUI() )
3812 mModelForItemFunctions = q;
3813 // This call would destroy the expanded state of items.
3814 // This is why when mModelForItemFunctions was 0 we didn't actually expand them
3815 // but we just set a "ExpandNeeded" mark...
3816 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3817 QTime layoutChangedTimer;
3818 layoutChangedTimer.start();
3819 #endif
3820 mView->modelAboutToEmitLayoutChanged();
3821 emit q->layoutChanged();
3822 mView->modelEmittedLayoutChanged();
3824 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3825 Stats::layoutChangeTime = layoutChangedTimer.elapsed();
3826 QTime expandingTime;
3827 expandingTime.start();
3828 #endif
3830 // expand all the items that need it in a single sweep
3832 // FIXME: This takes quite a lot of time, it could be made an interruptible job
3834 QList< Item * > * rootChildItems = mRootItem->childItems();
3835 if ( rootChildItems )
3837 for ( QList< Item * >::Iterator it = rootChildItems->begin(); it != rootChildItems->end() ;++it )
3839 if ( ( *it )->initialExpandStatus() == Item::ExpandNeeded )
3840 syncExpandedStateOfSubtree( *it );
3843 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3844 Stats::expandingTreeTime = expandingTime.elapsed();
3845 #endif
3846 } else {
3847 mView->ignoreUpdateGeometries( false );
3848 // explicit call to updateGeometries() here
3849 mView->updateGeometries();
3852 // this job has been completed
3853 delete mViewItemJobs.takeFirst();
3855 #ifdef KDEPIM_FOLDEROPEN_PROFILE
3856 // Last job finished!
3857 Stats::totalTime[currentPass] = Stats::currentJobStartTime.elapsed();
3858 printStatistics();
3859 #endif
3861 // take care of small jobs which never timeout by themselves because
3862 // of a small number of messages. At the end of each job check
3863 // the time used and if we're timeoutting and there is another job
3864 // then interrupt.
3865 elapsed = tStart.msecsTo( QTime::currentTime() );
3866 if ( ( elapsed > mViewItemJobStepChunkTimeout ) || ( elapsed < 0 ) )
3868 if ( !mViewItemJobs.isEmpty() )
3869 return ViewItemJobInterrupted;
3870 // else it's completed in fact
3871 } // else proceed with the next job
3873 break;
3874 default:
3875 // This is *really* a BUG
3876 kWarning() << "ERROR: returned an invalid result";
3877 Q_ASSERT( false );
3878 break;
3882 // no more jobs
3884 emit q->statusMessage( i18nc( "@info:status Finished view fill", "Ready" ) );
3886 return ViewItemJobCompleted;
3890 void ModelPrivate::viewItemJobStep()
3892 // A single step in the View Fill operation.
3893 // This function wraps viewItemJobStepInternal() which does the step job
3894 // and either completes it or stops because of a timeout.
3895 // If the job is stopped then we start a zero-msecs timer to call us
3896 // back and resume the job. Otherwise we're just done.
3898 mViewItemJobStepStartTime = ::time( 0 );
3900 if( mFillStepTimer.isActive() )
3901 mFillStepTimer.stop();
3903 if ( !mStorageModel )
3904 return; // nothing more to do
3907 // Save the current item in the view as our process may
3908 // cause items to be reparented (and QTreeView will forget the current item in the meantime).
3909 // This machinery is also needed when we're about to remove items from the view in
3910 // a cleanup job: we'll be trying to set as current the item after the one removed.
3912 QModelIndex currentIndexBeforeStep = mView->currentIndex();
3913 Item * currentItemBeforeStep = currentIndexBeforeStep.isValid() ?
3914 static_cast< Item * >( currentIndexBeforeStep.internalPointer() ) : 0;
3916 // mCurrentItemToRestoreAfterViewItemJobStep will be zeroed out if it's killed
3917 mCurrentItemToRestoreAfterViewItemJobStep = currentItemBeforeStep;
3919 // Save the current item position in the viewport as QTreeView fails to keep
3920 // the current item in the sample place when items are added or removed...
3921 QRect rectBeforeViewItemJobStep;
3923 // There is another popular requisite: people want the view to automatically
3924 // scroll in order to show new arriving mail. This actually makes sense
3925 // only when the view is sorted by date and the new mail is (usually) either
3926 // appended at the bottom or inserted at the top. It would be also confusing
3927 // when the user is browsing some other thread in the meantime.
3929 // So here we make a simple guess: if the view is scrolled somewhere in the
3930 // middle then we assume that the user is browsing other threads and we
3931 // try to keep the currently selected item steady on the screen.
3932 // When the view is "locked" to the top (scrollbar value 0) or to the
3933 // bottom (scrollbar value == maximum) then we assume that the user
3934 // isn't browsing and we should attempt to show the incoming messages
3935 // by keeping the view "locked".
3937 // The "locking" also doesn't make sense in the first big fill view job.
3939 int scrollBarPositionBeforeViewItemJobStep = mView->verticalScrollBar()->value();
3940 int scrollBarMaximumBeforeViewItemJobStep = mView->verticalScrollBar()->maximum();
3942 bool lockView = (
3943 // not the first loading job
3944 !mLoading
3945 ) && (
3946 // messages sorted by date
3947 ( mSortOrder->messageSorting() == SortOrder::SortMessagesByDateTime ) ||
3948 ( mSortOrder->messageSorting() == SortOrder::SortMessagesByDateTimeOfMostRecent )
3949 ) && (
3950 // scrollbar at top or bottom
3951 ( scrollBarPositionBeforeViewItemJobStep == 0 ) ||
3952 ( scrollBarPositionBeforeViewItemJobStep == scrollBarMaximumBeforeViewItemJobStep )
3955 // This is generally SLOW AS HELL... (so we avoid it if we lock the view and thus don't need it)
3956 if ( mCurrentItemToRestoreAfterViewItemJobStep && ( !lockView ) )
3957 rectBeforeViewItemJobStep = mView->visualRect( currentIndexBeforeStep );
3959 // FIXME: If the current item is NOT in the view, preserve the position
3960 // of the top visible item. This will make the view move yet less.
3962 // Insulate the View from (very likely spurious) "currentChanged()" signals.
3963 mView->ignoreCurrentChanges( true );
3965 // And go to real work.
3966 switch( viewItemJobStepInternal() )
3968 case ViewItemJobInterrupted:
3969 // Operation timed out, need to resume in a while
3970 if ( !mInLengthyJobBatch )
3972 mInLengthyJobBatch = true;
3973 mView->modelJobBatchStarted();
3975 mFillStepTimer.start( mViewItemJobStepIdleInterval ); // this is a single shot timer connected to viewItemJobStep()
3976 // and go dealing with current/selection out of the switch.
3977 break;
3978 case ViewItemJobCompleted:
3979 // done :)
3981 Q_ASSERT( mModelForItemFunctions ); // UI must be no (longer) disconnected in this state
3983 // Ask the view to remove the eventual busy indications
3984 if ( mInLengthyJobBatch )
3986 mInLengthyJobBatch = false;
3987 mView->modelJobBatchTerminated();
3990 if ( mLoading )
3992 mLoading = false;
3993 mView->modelFinishedLoading();
3996 // Apply pre-selection, if any
3997 if ( mPreSelectionMode != PreSelectNone )
3999 mView->ignoreCurrentChanges( false );
4001 bool bSelectionDone = false;
4003 switch( mPreSelectionMode )
4005 case PreSelectLastSelected:
4006 // fall down
4007 break;
4008 case PreSelectFirstUnreadCentered:
4009 bSelectionDone = mView->selectFirstMessageItem( MessageTypeUnreadOnly, true ); // center
4010 break;
4011 case PreSelectOldestCentered:
4012 mView->setCurrentMessageItem( mOldestItem, true /* center */ );
4013 bSelectionDone = true;
4014 break;
4015 case PreSelectNewestCentered:
4016 mView->setCurrentMessageItem( mNewestItem, true /* center */ );
4017 bSelectionDone = true;
4018 break;
4019 case PreSelectNone:
4020 // deal with selection below
4021 break;
4022 default:
4023 kWarning() << "ERROR: Unrecognized pre-selection mode " << (int)mPreSelectionMode;
4024 break;
4027 if ( ( !bSelectionDone ) && ( mPreSelectionMode != PreSelectNone ) )
4029 // fallback to last selected, if possible
4030 if ( mLastSelectedMessageInFolder ) // we found it in the loading process: select and jump out
4032 mView->setCurrentMessageItem( mLastSelectedMessageInFolder );
4033 bSelectionDone = true;
4037 mUniqueIdOfLastSelectedMessageInFolder = 0;
4038 mLastSelectedMessageInFolder = 0;
4039 mPreSelectionMode = PreSelectNone;
4041 if ( bSelectionDone )
4042 return; // already taken care of current / selection
4044 // deal with current/selection out of the switch
4046 break;
4047 default:
4048 // This is *really* a BUG
4049 kWarning() << "ERROR: returned an invalid result";
4050 Q_ASSERT( false );
4051 break;
4054 // Everything else here deals with the selection
4056 // If UI is disconnected then we don't have anything else to do here
4057 if ( !mModelForItemFunctions )
4059 mView->ignoreCurrentChanges( false );
4060 return;
4063 // Restore current/selection and/or scrollbar position
4065 if ( mCurrentItemToRestoreAfterViewItemJobStep )
4067 bool stillIgnoringCurrentChanges = true;
4069 // If the assert below fails then the previously current item got detached
4070 // and didn't get reattached in the step: this should never happen.
4071 Q_ASSERT( mCurrentItemToRestoreAfterViewItemJobStep->isViewable() );
4073 // Check if the current item changed
4074 QModelIndex currentIndexAfterStep = mView->currentIndex();
4075 Item * currentAfterStep = currentIndexAfterStep.isValid() ?
4076 static_cast< Item * >( currentIndexAfterStep.internalPointer() ) : 0;
4078 if ( mCurrentItemToRestoreAfterViewItemJobStep != currentAfterStep )
4080 // QTreeView lost the current item...
4081 if ( mCurrentItemToRestoreAfterViewItemJobStep != currentItemBeforeStep )
4083 // Some view job code expects us to actually *change* the current item.
4084 // This is done by the cleanup step which removes items and tries
4085 // to set as current the item *after* the removed one, if possible.
4086 // We need the view to handle the change though.
4087 stillIgnoringCurrentChanges = false;
4088 mView->ignoreCurrentChanges( false );
4089 } else {
4090 // we just have to restore the old current item. The code
4091 // outside shouldn't have noticed that we lost it (e.g. the message viewer
4092 // still should have the old message opened). So we don't need to
4093 // actually notify the view of the restored setting.
4095 // Restore it
4096 kDebug() << "Gonna restore current here" << mCurrentItemToRestoreAfterViewItemJobStep->subject();
4097 mView->setCurrentIndex( q->index( mCurrentItemToRestoreAfterViewItemJobStep, 0 ) );
4098 } else {
4099 // The item we're expected to set as current is already current
4100 if ( mCurrentItemToRestoreAfterViewItemJobStep != currentItemBeforeStep )
4102 // But we have changed it in the job step.
4103 // This means that: we have deleted the current item and chosen a
4104 // new candidate as current but Qt also has chosen it as candidate
4105 // and already made it current. The problem is that (as of Qt 4.4)
4106 // it probably didn't select it.
4107 if ( !mView->selectionModel()->hasSelection() )
4109 stillIgnoringCurrentChanges = false;
4110 mView->ignoreCurrentChanges( false );
4112 kDebug() << "Gonna restore selection here" << mCurrentItemToRestoreAfterViewItemJobStep->subject();
4114 QItemSelection selection;
4115 selection.append( QItemSelectionRange( q->index( mCurrentItemToRestoreAfterViewItemJobStep, 0 ) ) );
4116 mView->selectionModel()->select( selection, QItemSelectionModel::Select | QItemSelectionModel::Rows );
4121 // FIXME: If it was selected before the change, then re-select it (it may happen that it's not)
4122 if ( lockView )
4124 // we prefer to keep the view locked to the top or bottom
4125 if ( scrollBarPositionBeforeViewItemJobStep != 0 )
4127 // we wanted the view to be locked to the bottom
4128 if ( mView->verticalScrollBar()->value() != mView->verticalScrollBar()->maximum() )
4129 mView->verticalScrollBar()->setValue( mView->verticalScrollBar()->maximum() );
4130 } // else we wanted the view to be locked to top and we shouldn't need to do anything
4131 } else {
4132 // we prefer to keep the currently selected item steady in the view
4133 QRect rectAfterViewItemJobStep = mView->visualRect( q->index( mCurrentItemToRestoreAfterViewItemJobStep, 0 ) );
4134 if ( rectBeforeViewItemJobStep.y() != rectAfterViewItemJobStep.y() )
4136 // QTreeView lost its position...
4137 mView->verticalScrollBar()->setValue( mView->verticalScrollBar()->value() + rectAfterViewItemJobStep.y() - rectBeforeViewItemJobStep.y() );
4141 // and kill the insulation, if not yet done
4142 if ( stillIgnoringCurrentChanges )
4143 mView->ignoreCurrentChanges( false );
4145 return;
4148 // Either there was no current item before, or it was lost in a cleanup step and another candidate for
4149 // current item couldn't be found (possibly empty view)
4150 mView->ignoreCurrentChanges( false );
4152 if ( currentItemBeforeStep )
4154 // lost in a cleanup..
4155 // tell the view that we have a new current, this time with no insulation
4156 mView->slotSelectionChanged( QItemSelection(), QItemSelection() );
4159 if ( lockView )
4161 if ( scrollBarPositionBeforeViewItemJobStep != 0 )
4163 // we wanted the view to be locked to the bottom
4164 if ( mView->verticalScrollBar()->value() != mView->verticalScrollBar()->maximum() )
4165 mView->verticalScrollBar()->setValue( mView->verticalScrollBar()->maximum() );
4166 } // else we wanted the view to be locked to top and we shouldn't need to do anything
4170 void ModelPrivate::slotStorageModelRowsInserted( const QModelIndex &parent, int from, int to )
4172 if ( parent.isValid() )
4173 return; // ugh... should never happen
4175 Q_ASSERT( from <= to );
4177 int count = ( to - from ) + 1;
4179 mInvariantRowMapper->modelRowsInserted( from, count );
4181 // look if no current job is in the middle
4183 int jobCount = mViewItemJobs.count();
4185 for ( int idx = 0; idx < jobCount; idx++ )
4187 ViewItemJob * job = mViewItemJobs.at( idx );
4188 if ( job->currentPass() == ViewItemJob::Pass1Fill )
4191 // The following cases are possible:
4193 // from to
4194 // | | -> shift up job
4195 // from to
4196 // | | -> shift up job
4197 // from to
4198 // | | -> shift up job
4199 // from to
4200 // | | -> split job
4201 // from to
4202 // | | -> split job
4203 // from to
4204 // | | -> job unaffected
4207 // FOLDER
4208 // |-------------------------|---------|--------------|
4209 // 0 currentIndex endIndex count
4210 // +-- job --+
4214 if ( from > job->endIndex() )
4216 // The change is completely above the job, the job is not affected
4217 } else if( from > job->currentIndex() ) // and from <= job->endIndex()
4219 // The change starts in the middle of the job in a way that it must be split in two.
4220 // The first part is unaffected by the shift and ranges from job->currentIndex() to from - 1.
4221 // We use the existing job for this.
4222 job->setEndIndex( from - 1 );
4224 Q_ASSERT( job->currentIndex() <= job->endIndex() );
4226 // The second part would range from "from" to job->endIndex() but must
4227 // be shifted up by count. We add a new job for this.
4228 ViewItemJob * newJob = new ViewItemJob( from + count, job->endIndex() + count, job->chunkTimeout(), job->idleInterval(), job->messageCheckCount() );
4230 Q_ASSERT( newJob->currentIndex() <= newJob->endIndex() );
4232 idx++; // we can skip this job in the loop, it's already ok
4233 jobCount++; // and our range increases by one.
4234 mViewItemJobs.insert( idx, newJob );
4236 } else {
4237 // The change starts below (or exactly on the beginning of) the job.
4238 // The job must be shifted up.
4239 job->setCurrentIndex( job->currentIndex() + count );
4240 job->setEndIndex( job->endIndex() + count );
4242 Q_ASSERT( job->currentIndex() <= job->endIndex() );
4244 } else {
4245 // The job is a cleanup or in a later pass: the storage has been already accessed
4246 // and the messages created... no need to care anymore: the invariant row mapper will do the job.
4250 bool newJobNeeded = true;
4252 // Try to attach to an existing fill job, if any.
4253 // To enforce consistency we can attach only if the Fill job
4254 // is the last one in the list (might be eventually *also* the first,
4255 // and even being already processed but we must make sure that there
4256 // aren't jobs _after_ it).
4257 if ( jobCount > 0 )
4259 ViewItemJob * job = mViewItemJobs.at( jobCount - 1 );
4260 if ( job->currentPass() == ViewItemJob::Pass1Fill )
4262 if (
4263 // The job ends just before the added rows
4264 ( from == ( job->endIndex() + 1 ) ) &&
4265 // The job didn't reach the end of Pass1Fill yet
4266 ( job->currentIndex() <= job->endIndex() )
4269 // We can still attach this :)
4270 job->setEndIndex( to );
4271 Q_ASSERT( job->currentIndex() <= job->endIndex() );
4272 newJobNeeded = false;
4277 if ( newJobNeeded )
4279 // FIXME: Should take timing options from aggregation here ?
4280 ViewItemJob * job = new ViewItemJob( from, to, 100, 50, 10 );
4281 mViewItemJobs.append( job );
4284 if ( !mFillStepTimer.isActive() )
4285 mFillStepTimer.start( mViewItemJobStepIdleInterval );
4288 void ModelPrivate::slotStorageModelRowsRemoved( const QModelIndex &parent, int from, int to )
4290 // This is called when the underlying StorageModel emits the rowsRemoved signal.
4292 if ( parent.isValid() )
4293 return; // ugh... should never happen
4295 // look if no current job is in the middle
4297 Q_ASSERT( from <= to );
4299 int count = ( to - from ) + 1;
4301 int jobCount = mViewItemJobs.count();
4303 for ( int idx = 0; idx < jobCount; idx++ )
4305 ViewItemJob * job = mViewItemJobs.at( idx );
4306 if ( job->currentPass() == ViewItemJob::Pass1Fill )
4309 // The following cases are possible:
4311 // from to
4312 // | | -> shift down job
4313 // from to
4314 // | | -> shift down and crop job
4315 // from to
4316 // | | -> kill job
4317 // from to
4318 // | | -> split job, crop and shift
4319 // from to
4320 // | | -> crop job
4321 // from to
4322 // | | -> job unaffected
4325 // FOLDER
4326 // |-------------------------|---------|--------------|
4327 // 0 currentIndex endIndex count
4328 // +-- job --+
4332 if ( from > job->endIndex() )
4334 // The change is completely above the job, the job is not affected
4335 } else if( from > job->currentIndex() ) // and from <= job->endIndex()
4337 // The change starts in the middle of the job and ends in the middle or after the job.
4339 // The first part is unaffected by the shift and ranges from job->currentIndex() to from - 1
4340 // We use the existing job for this.
4341 job->setEndIndex( from - 1 ); // stop before the first removed row
4343 Q_ASSERT( job->currentIndex() <= job->endIndex() );
4345 if ( to < job->endIndex() )
4347 // The change ends inside the job and a part of it can be completed.
4348 // We create a new job for the shifted remaining part. It would actually
4349 // range from to + 1 up to job->endIndex(), but we need to shift it down by count.
4350 // since count = ( to - from ) + 1 so from = to + 1 - count
4352 ViewItemJob * newJob = new ViewItemJob( from, job->endIndex() - count, job->chunkTimeout(), job->idleInterval(), job->messageCheckCount() );
4354 Q_ASSERT( newJob->currentIndex() < newJob->endIndex() );
4356 idx++; // we can skip this job in the loop, it's already ok
4357 jobCount++; // and our range increases by one.
4358 mViewItemJobs.insert( idx, newJob );
4359 } // else the change includes completely the end of the job and no other part of it can be completed.
4360 } else {
4361 // The change starts below (or exactly on the beginning of) the job. ( from <= job->currentIndex() )
4362 if ( to >= job->endIndex() )
4364 // The change completely covers the job: kill it
4366 // We don't delete the job since we want the other passes to be completed
4367 // This is because the Pass1Fill may have already filled mUnassignedMessageListForPass2
4368 // and may have set mOldestItem and mNewestItem. We *COULD* clear the unassigned
4369 // message list with clearUnassignedMessageLists() but mOldestItem and mNewestItem
4370 // could be still dangling pointers. So we just move the current index of the job
4371 // after the end (so storage model scan terminates) and let it complete spontaneously.
4372 job->setCurrentIndex( job->endIndex() + 1 );
4374 } else if ( to >= job->currentIndex() )
4376 // The change partially covers the job. Only a part of it can be completed
4377 // and it must be shifted down. It would actually
4378 // range from to + 1 up to job->endIndex(), but we need to shift it down by count.
4379 // since count = ( to - from ) + 1 so from = to + 1 - count
4380 job->setCurrentIndex( from );
4381 job->setEndIndex( job->endIndex() - count );
4383 Q_ASSERT( job->currentIndex() <= job->endIndex() );
4384 } else {
4385 // The change is completely below the job: it must be shifted down.
4386 job->setCurrentIndex( job->currentIndex() - count );
4387 job->setEndIndex( job->endIndex() - count );
4390 } else {
4391 // The job is a cleanup or in a later pass: the storage has been already accessed
4392 // and the messages created... no need to care: we will invalidate the messages in a while.
4396 // This will invalidate the ModelInvariantIndex-es that have been removed and return
4397 // them all in a nice list that we can feed to a view removal job.
4398 QList< ModelInvariantIndex * > * invalidatedIndexes = mInvariantRowMapper->modelRowsRemoved( from, count );
4400 if ( invalidatedIndexes )
4402 // Try to attach to an existing cleanup job, if any.
4403 // To enforce consistency we can attach only if the Cleanup job
4404 // is the last one in the list (might be eventually *also* the first,
4405 // and even being already processed but we must make sure that there
4406 // aren't jobs _after_ it).
4407 if ( jobCount > 0 )
4409 ViewItemJob * job = mViewItemJobs.at( jobCount - 1 );
4410 if ( job->currentPass() == ViewItemJob::Pass1Cleanup )
4412 if ( ( job->currentIndex() <= job->endIndex() ) && job->invariantIndexList() )
4414 //kDebug() << "Appending " << invalidatedIndexes->count() << " invalidated indexes to existing cleanup job" << endl;
4415 // We can still attach this :)
4416 *( job->invariantIndexList() ) += *invalidatedIndexes;
4417 job->setEndIndex( job->endIndex() + invalidatedIndexes->count() );
4418 delete invalidatedIndexes;
4419 invalidatedIndexes = 0;
4424 if ( invalidatedIndexes )
4426 // Didn't append to any existing cleanup job.. create a new one
4428 //kDebug() << "Creating new cleanup job for " << invalidatedIndexes->count() << " invalidated indexes" << endl;
4429 // FIXME: Should take timing options from aggregation here ?
4430 ViewItemJob * job = new ViewItemJob( ViewItemJob::Pass1Cleanup, invalidatedIndexes, 100, 50, 10 );
4431 mViewItemJobs.append( job );
4434 if ( !mFillStepTimer.isActive() )
4435 mFillStepTimer.start( mViewItemJobStepIdleInterval );
4439 void ModelPrivate::slotStorageModelLayoutChanged()
4441 kDebug() << "Storage model layout changed";
4442 // need to reset everything...
4443 q->setStorageModel( mStorageModel );
4444 kDebug() << "Storage model layout changed done";
4447 void ModelPrivate::slotStorageModelDataChanged( const QModelIndex &fromIndex, const QModelIndex &toIndex )
4449 Q_ASSERT( mStorageModel ); // must exist (and be the sender of the signal connected to this slot)
4451 int from = fromIndex.row();
4452 int to = toIndex.row();
4454 Q_ASSERT( from <= to );
4456 int count = ( to - from ) + 1;
4458 int jobCount = mViewItemJobs.count();
4460 // This will find out the ModelInvariantIndex-es that need an update and will return
4461 // them all in a nice list that we can feed to a view removal job.
4462 QList< ModelInvariantIndex * > * indexesThatNeedUpdate = mInvariantRowMapper->modelIndexRowRangeToModelInvariantIndexList( from, count );
4464 if ( indexesThatNeedUpdate )
4466 // Try to attach to an existing update job, if any.
4467 // To enforce consistency we can attach only if the Update job
4468 // is the last one in the list (might be eventually *also* the first,
4469 // and even being already processed but we must make sure that there
4470 // aren't jobs _after_ it).
4471 if ( jobCount > 0 )
4473 ViewItemJob * job = mViewItemJobs.at( jobCount - 1 );
4474 if ( job->currentPass() == ViewItemJob::Pass1Update )
4476 if ( ( job->currentIndex() <= job->endIndex() ) && job->invariantIndexList() )
4478 // We can still attach this :)
4479 *( job->invariantIndexList() ) += *indexesThatNeedUpdate;
4480 job->setEndIndex( job->endIndex() + indexesThatNeedUpdate->count() );
4481 delete indexesThatNeedUpdate;
4482 indexesThatNeedUpdate = 0;
4487 if ( indexesThatNeedUpdate )
4489 // Didn't append to any existing update job.. create a new one
4490 // FIXME: Should take timing options from aggregation here ?
4491 ViewItemJob * job = new ViewItemJob( ViewItemJob::Pass1Update, indexesThatNeedUpdate, 100, 50, 10 );
4492 mViewItemJobs.append( job );
4495 if ( !mFillStepTimer.isActive() )
4496 mFillStepTimer.start( mViewItemJobStepIdleInterval );
4501 void ModelPrivate::slotStorageModelHeaderDataChanged( Qt::Orientation, int, int )
4503 if ( mStorageModelContainsOutboundMessages!=mStorageModel->containsOutboundMessages() ) {
4504 mStorageModelContainsOutboundMessages = mStorageModel->containsOutboundMessages();
4505 emit q->headerDataChanged( Qt::Horizontal, 0, q->columnCount() );
4509 Qt::ItemFlags Model::flags( const QModelIndex &index ) const
4511 if ( !index.isValid() )
4512 return Qt::NoItemFlags;
4514 Q_ASSERT( d->mModelForItemFunctions ); // UI must be connected if a valid index was queried
4516 Item * it = static_cast< Item * >( index.internalPointer() );
4518 Q_ASSERT( it );
4520 if ( it->type() == Item::GroupHeader )
4521 return Qt::ItemIsEnabled;
4523 Q_ASSERT( it->type() == Item::Message );
4525 if ( !static_cast< MessageItem * >( it )->isValid() )
4526 return Qt::NoItemFlags; // not enabled, not selectable
4528 if ( static_cast< MessageItem * >( it )->aboutToBeRemoved() )
4529 return Qt::NoItemFlags; // not enabled, not selectable
4531 if ( static_cast< MessageItem * >( it )->status().isDeleted() )
4532 return Qt::NoItemFlags; // not enabled, not selectable
4534 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
4537 QMimeData* MessageList::Core::Model::mimeData( const QModelIndexList& indexes ) const
4539 QList< MessageItem* > msgs;
4540 foreach( const QModelIndex &idx, indexes ) {
4541 if( idx.isValid() ) {
4542 Item* item = static_cast< Item* >( idx.internalPointer() );
4543 if( item->type() == MessageList::Core::Item::Message ) {
4544 msgs << static_cast< MessageItem* >( idx.internalPointer() );
4548 return storageModel()->mimeData( msgs );
4552 Item *Model::rootItem() const
4554 return d->mRootItem;
4557 bool Model::isLoading() const
4559 return d->mLoading;
4562 MessageItem * Model::messageItemByStorageRow( int row ) const
4564 if ( !d->mStorageModel )
4565 return 0;
4566 ModelInvariantIndex * idx = d->mInvariantRowMapper->modelIndexRowToModelInvariantIndex( row );
4567 if ( !idx )
4568 return 0;
4570 return static_cast< MessageItem * >( idx );
4574 MessageItemSetReference Model::createPersistentSet( const QList< MessageItem * > &items )
4576 if ( !d->mPersistentSetManager )
4577 d->mPersistentSetManager = new MessageItemSetManager();
4579 MessageItemSetReference ref = d->mPersistentSetManager->createSet();
4580 for ( QList< MessageItem * >::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it )
4581 d->mPersistentSetManager->addMessageItem( ref, *it );
4583 return ref;
4586 QList< MessageItem * > Model::persistentSetCurrentMessageItemList( MessageItemSetReference ref )
4588 if ( !d->mPersistentSetManager )
4589 return QList< MessageItem * >();
4591 return d->mPersistentSetManager->messageItems( ref );
4594 void Model::deletePersistentSet( MessageItemSetReference ref )
4596 if ( !d->mPersistentSetManager )
4597 return;
4599 d->mPersistentSetManager->removeSet( ref );
4601 if ( d->mPersistentSetManager->setCount() < 1 )
4603 delete d->mPersistentSetManager;
4604 d->mPersistentSetManager = 0;
4608 #include "model.moc"