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>
66 #include <KCalendarSystem>
77 K_GLOBAL_STATIC( QTimer
, _k_heartBeatTimer
)
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
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.
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
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
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
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.
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
;
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
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 ) {};
228 delete mInvariantIndexList
;
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
; };
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
; };
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;
275 d
->mPersistentSetManager
= 0;
276 d
->mInLengthyJobBatch
= false;
277 d
->mUniqueIdOfLastSelectedMessageInFolder
= 0;
278 d
->mLastSelectedMessageInFolder
= 0;
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",
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
313 setStorageModel( 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
;
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
)
342 void Model::setSortOrder( const SortOrder
* sortOrder
)
344 d
->mSortOrder
= sortOrder
;
347 void Model::setFilter( const Filter
*filter
)
351 QList
< Item
* > * childList
= d
->mRootItem
->childItems();
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 );
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 );
399 mView
->setRowHidden( thisIndex
.row(), parentIndex
, false );
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
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
);
439 if ( item
->type() == Item::Message
)
441 if ( mFilter
->match( ( MessageItem
* )item
) )
443 mView
->setRowHidden( thisIndex
.row(), parentIndex
, false );
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 );
454 int Model::columnCount( const QModelIndex
& parent
) const
458 if ( parent
.column() > 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
470 Item
* item
= static_cast< Item
* >( index
.internalPointer() );
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() );
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() );
488 case Qt::UserRole
+ 3: //EntityTreeModel::MimeTypeRole
489 if( item
->type() == MessageList::Core::Item::Message
)
490 return QLatin1String( "message/rfc822" );
499 QVariant
Model::headerData(int section
, Qt::Orientation
, int role
) const
504 Theme::Column
* column
= d
->mTheme
->column( section
);
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() ) );
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
534 return QModelIndex();
536 // FIXME: This function is a bottleneck
537 Item
* par
= item
->parent();
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
563 return QModelIndex(); // senseless column (we could optimize by skipping this check but ModelTest from trolltech is pedantic)
567 if ( parent
.isValid() )
569 item
= static_cast< const Item
* >( parent
.internalPointer() );
571 return QModelIndex(); // should never happen
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
);
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() );
593 return QModelIndex();
594 Item
*par
= item
->parent();
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
607 if ( parent
.isValid() )
609 item
= static_cast< const Item
* >( parent
.internalPointer() );
611 return 0; // should never happen
616 if ( !item
->isViewable() )
619 return item
->childItemCount();
622 class RecursionPreventer
625 RecursionPreventer( int &counter
)
626 : mCounter( counter
) { mCounter
++; }
627 ~RecursionPreventer() { mCounter
--; }
628 bool isRecursive() const { return mCounter
> 1; }
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() )
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;
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();
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
;
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).
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
=
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
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.
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
);
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
);
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
);
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
);
870 kWarning() << "Unrecognized fill view strategy";
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
900 if ( !mViewItemJobs
.isEmpty() )
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
);
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();
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
)
1015 mUnassignedMessageListForPass2
.clear();
1016 // Any message these list contain was also in mUnassignedMessageListForPass2
1017 mUnassignedMessageListForPass3
.clear();
1018 mUnassignedMessageListForPass4
.clear();
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
)
1053 mUnassignedMessageListForPass3
.clear();
1054 mUnassignedMessageListForPass4
.clear();
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
)
1071 mUnassignedMessageListForPass3
.clear();
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
)
1092 mUnassignedMessageListForPass4
.clear();
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).
1111 mOrphanChildrenHash
.clear();
1114 void ModelPrivate::clearJobList()
1116 if ( mViewItemJobs
.isEmpty() )
1119 if ( mInLengthyJobBatch
)
1121 mInLengthyJobBatch
= false;
1122 mView
->modelJobBatchTerminated();
1125 for( QList
< ViewItemJob
* >::Iterator it
= mViewItemJobs
.begin();
1126 it
!= mViewItemJobs
.end() ; ++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() )
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 ); \
1162 case SortOrder::Descending: \
1163 mRootItem->d_ptr->insertChildItem< _ItemComparator, false >( mModelForItemFunctions, ghi ); \
1165 default: /* should never happen... */ \
1166 mRootItem->appendChildItem( mModelForItemFunctions, ghi ); \
1170 switch( mSortOrder
->groupSorting() )
1172 case SortOrder::SortGroupsByDateTime
:
1173 INSERT_GROUP_WITH_COMPARATOR( ItemDateComparator
)
1175 case SortOrder::SortGroupsByDateTimeOfMostRecent
:
1176 INSERT_GROUP_WITH_COMPARATOR( ItemMaxDateComparator
)
1178 case SortOrder::SortGroupsBySenderOrReceiver
:
1179 INSERT_GROUP_WITH_COMPARATOR( ItemSenderOrReceiverComparator
)
1181 case SortOrder::SortGroupsBySender
:
1182 INSERT_GROUP_WITH_COMPARATOR( ItemSenderComparator
)
1184 case SortOrder::SortGroupsByReceiver
:
1185 INSERT_GROUP_WITH_COMPARATOR( ItemReceiverComparator
)
1187 case SortOrder::NoGroupSorting
:
1188 mRootItem
->appendChildItem( mModelForItemFunctions
, ghi
);
1190 default: // should never happen
1191 mRootItem
->appendChildItem( mModelForItemFunctions
, ghi
);
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.
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
1214 root
->setInitialExpandStatus( Item::ExpandNeeded
);
1216 QList
< Item
* > * children
= root
->childItems();
1220 for( QList
< Item
* >::Iterator it
= children
->begin(); it
!= children
->end(); ++it
)
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
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();
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
)
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();
1280 dt
.setTime_t( date
);
1281 QDate dDate
= dt
.date();
1282 const KCalendarSystem
*calendar
= KGlobal::locale()->calendar();
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;
1311 case 0: // This week
1312 groupLabel
= KGlobal::locale()->calendar()->weekDayName( dDate
);
1314 case 1: // 1 week ago
1315 groupLabel
= mCachedLastWeekLabel
;
1318 groupLabel
= mCachedTwoWeeksAgoLabel
;
1321 groupLabel
= mCachedThreeWeeksAgoLabel
;
1324 groupLabel
= mCachedFourWeeksAgoLabel
;
1327 groupLabel
= mCachedFiveWeeksAgoLabel
;
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
) );
1340 case Aggregation::GroupBySenderOrReceiver
:
1342 groupLabel
= MessageCore::StringUtil::stripEmailAddr( mi
->senderOrReceiver() );
1345 case Aggregation::GroupBySender
:
1347 groupLabel
= MessageCore::StringUtil::stripEmailAddr( mi
->sender() );
1350 case Aggregation::GroupByReceiver
:
1352 groupLabel
= MessageCore::StringUtil::stripEmailAddr( mi
->receiver() );
1355 case Aggregation::NoGrouping
:
1356 // append directly to root
1357 attachMessageToParent( mRootItem
, mi
);
1361 // should never happen
1362 attachMessageToParent( mRootItem
, mi
);
1366 GroupHeaderItem
* ghi
;
1368 ghi
= mGroupHeaderItemHash
.value( groupLabel
, 0 );
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
:
1381 case Aggregation::AlwaysExpandGroups
:
1383 ghi
->setInitialExpandStatus( Item::ExpandNeeded
);
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
);
1392 if ( ( ghi
->date() - mViewItemJobStepStartTime
) < ( 3600 * 72 ) )
1393 ghi
->setInitialExpandStatus( Item::ExpandNeeded
);
1401 attachMessageToParent( ghi
, mi
);
1403 attachGroup( ghi
); // this will expand the group if required
1405 mGroupHeaderItemHash
.insert( groupLabel
, ghi
);
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 );
1453 // Take care of circular references
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 );
1498 // Take care of circular references
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
);
1537 // Subject threading cache stuff
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
;
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
1574 inline bool operator()( const MessageItem
* mi1
, const MessageItem
* mi2
) const
1576 if ( mi1
->date() < mi2
->date() ) // likely
1578 if ( mi1
->date() > mi2
->date() ) // likely
1580 // dates are equal, compare by pointer
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
);
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
);
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
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();
1729 mi
->setThreadingStatus( MessageItem::ImperfectParentFound
);
1730 return pParent
; // got an imperfect parent for this message
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
)
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.
1784 ( propertyChangeMask
& MaxDateChanged
) &&
1785 // groups sorted by max date
1786 ( mSortOrder
->groupSorting() == SortOrder::SortGroupsByDateTimeOfMostRecent
)
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
) );
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
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
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
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
1840 // this kind of message sorting isn't affected by the property changes: nothing to do.
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.
1857 ( propertyChangeMask
& MaxDateChanged
) &&
1858 // thread leader is most recent message
1859 ( mAggregation
->threadLeader() == Aggregation::MostRecentMessage
)
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.
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
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
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
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
1915 // this kind of message sorting isn't affected by property changes: nothing to do.
1919 return true; // parent might be affected too.
1922 void ModelPrivate::messageDetachedUpdateParentProperties( Item
*oldParent
, MessageItem
*mi
)
1924 Q_ASSERT( oldParent
);
1926 Q_ASSERT( oldParent
!= mRootItem
);
1929 // oldParent might have its properties changed because of the child removal.
1930 // propagate the changes up.
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
;
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.
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
;
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
);
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
;
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.
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
;
2017 void ModelPrivate::attachMessageToParent( Item
*pParent
, MessageItem
*mi
)
2019 Q_ASSERT( pParent
);
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
;
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
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...
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
);
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
);
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
);
2107 case MessageItem::NonThreadable
: // this also happens when we do no threading at all
2109 Q_ASSERT( !mThreadingCacheMessageInReplyToIdMD5ToMessageItem
.contains( mi
->inReplyToIdMD5(), mi
) );
2114 // Set the new parent
2115 mi
->setParent( pParent
);
2117 // Propagate watched and ignored status
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 ); \
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
)
2165 case SortOrder::SortMessagesByDateTimeOfMostRecent
:
2166 INSERT_MESSAGE_WITH_COMPARATOR( ItemMaxDateComparator
)
2168 case SortOrder::SortMessagesBySize
:
2169 INSERT_MESSAGE_WITH_COMPARATOR( ItemSizeComparator
)
2171 case SortOrder::SortMessagesBySenderOrReceiver
:
2172 INSERT_MESSAGE_WITH_COMPARATOR( ItemSenderOrReceiverComparator
)
2174 case SortOrder::SortMessagesBySender
:
2175 INSERT_MESSAGE_WITH_COMPARATOR( ItemSenderComparator
)
2177 case SortOrder::SortMessagesByReceiver
:
2178 INSERT_MESSAGE_WITH_COMPARATOR( ItemReceiverComparator
)
2180 case SortOrder::SortMessagesBySubject
:
2181 INSERT_MESSAGE_WITH_COMPARATOR( ItemSubjectComparator
)
2183 case SortOrder::SortMessagesByActionItemStatus
:
2184 INSERT_MESSAGE_WITH_COMPARATOR( ItemActionItemStatusComparator
)
2186 case SortOrder::SortMessagesByUnreadStatus
:
2187 INSERT_MESSAGE_WITH_COMPARATOR( ItemUnreadStatusComparator
)
2189 case SortOrder::NoMessageSorting
:
2190 pParent
->appendChildItem( mModelForItemFunctions
, mi
);
2192 default: // should never happen
2193 pParent
->appendChildItem( mModelForItemFunctions
, mi
);
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
);
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
);
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
);
2221 case Aggregation::AlwaysExpandThreads
:
2222 // expand everything
2223 pParent
->setInitialExpandStatus( Item::ExpandNeeded
);
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
)
2247 mView
->expand( q
->index( parentToExpand
, 0 ) );
2249 parentToExpand
->setInitialExpandStatus( Item::ExpandExecuted
);
2250 parentToExpand
= parentToExpand
->parent();
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
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
)
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
)
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.
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;
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 ); \
2363 case SortOrder::Descending: \
2364 needsReSorting = ( *it )->parent()->d_ptr->childItemNeedsReSorting< _ItemDateComparator, false >( *it ); \
2366 default: /* should never happen */ \
2367 needsReSorting = false; \
2371 switch ( mSortOrder
->groupSorting() )
2373 case SortOrder::SortGroupsByDateTime
:
2374 CHECK_IF_GROUP_NEEDS_RESORTING( ItemDateComparator
)
2376 case SortOrder::SortGroupsByDateTimeOfMostRecent
:
2377 CHECK_IF_GROUP_NEEDS_RESORTING( ItemMaxDateComparator
)
2379 case SortOrder::SortGroupsBySenderOrReceiver
:
2380 CHECK_IF_GROUP_NEEDS_RESORTING( ItemSenderOrReceiverComparator
)
2382 case SortOrder::SortGroupsBySender
:
2383 CHECK_IF_GROUP_NEEDS_RESORTING( ItemSenderComparator
)
2385 case SortOrder::SortGroupsByReceiver
:
2386 CHECK_IF_GROUP_NEEDS_RESORTING( ItemReceiverComparator
)
2388 case SortOrder::NoGroupSorting
:
2389 needsReSorting
= false;
2392 // Should never happen... just assume re-sorting is not needed
2393 needsReSorting
= false;
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();
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"
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
);
2453 // A parent was already assigned in Pass3: we have nothing to do here
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.
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
);
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
);
2525 // mi wasn't viewable yet.. no need to attach parent
2526 attachMessageToParent( mparent
, mi
);
2528 // and we're done for now
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
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
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() );
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"
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
);
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
);
2618 // mi wasn't viewable yet.. no need to attach parent
2619 attachMessageToParent( mparent
, mi
);
2621 // and we're done for now
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)
2632 // We're not doing subject based threading: will never be threaded, go straight to Pass4
2633 mUnassignedMessageListForPass4
.append( mi
); // this is ~O(1)
2636 case MessageItem::NonThreadable
:
2637 // will never be threaded, go straight to Pass4
2638 mUnassignedMessageListForPass4
.append( mi
); // this is ~O(1)
2642 kWarning() << "ERROR: Invalid message threading status returned by findMessageParent()!";
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() );
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"
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
2709 mi
= new MessageItem();
2711 // a MessageItem discarded by a previous iteration: reuse it.
2712 Q_ASSERT( mi
->parent() == 0 );
2715 if ( !mStorageModel
->initializeMessageItem( mi
, curIndex
, bUseReceiver
) )
2718 kWarning() << "Fill of the MessageItem at storage row index " << curIndex
<< " failed";
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() ) {
2742 if ( !mNewestItem
|| mNewestItem
->date() < mi
->date() ) {
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
);
2767 case Aggregation::PerfectAndReferences
:
2768 mStorageModel
->fillMessageItemThreadingData( mi
, curIndex
, StorageModel::PerfectThreadingPlusReferences
);
2771 mStorageModel
->fillMessageItemThreadingData( mi
, curIndex
, StorageModel::PerfectThreadingOnly
);
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).
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
);
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)
2851 // will have to wait Pass2
2852 mUnassignedMessageListForPass2
.append( mi
);
2855 // No In-Reply-To header.
2857 bool mightHaveOtherMeansForThreading
;
2859 switch( mAggregation
->threading() )
2861 case Aggregation::PerfectReferencesAndSubject
:
2862 mightHaveOtherMeansForThreading
= mi
->subjectIsPrefixed() || !mi
->referencesIdMD5().isEmpty();
2864 case Aggregation::PerfectAndReferences
:
2865 mightHaveOtherMeansForThreading
= !mi
->referencesIdMD5().isEmpty();
2867 case Aggregation::PerfectOnly
:
2868 mightHaveOtherMeansForThreading
= false;
2871 // BUG: there shouldn't be other values (NoThreading is excluded in an upper branch)
2873 mightHaveOtherMeansForThreading
= false; // make gcc happy
2877 if ( mightHaveOtherMeansForThreading
)
2879 // We might have other means for threading this message, wait until Pass2
2880 mUnassignedMessageListForPass2
.append( mi
);
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.
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)
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
);
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
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
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
);
2942 return ViewItemJobInterrupted
;
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.
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() );
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)
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() );
3080 Q_ASSERT( !mThreadingCacheMessageInReplyToIdMD5ToMessageItem
.contains( dyingMessage
->inReplyToIdMD5(), dyingMessage
) );
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
);
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.
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
) {
3137 if ( mOldestItem
== dyingMessage
) {
3141 delete dyingMessage
;
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();
3177 while ( it
!= mOrphanChildrenHash
.end() )
3179 mUnassignedMessageListForPass2
.append( *it
);
3181 mOrphanChildrenHash
.erase( it
);
3183 it
= mOrphanChildrenHash
.begin();
3185 // This is still interruptible
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.
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
);
3236 // Must have been invalidated (so it's basically about to be deleted)
3237 Q_ASSERT( !message
->isValid() );
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
);
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
3325 // Done updating this message
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...
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
;
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
3380 elapsed
= tStart
.msecsTo( QTime::currentTime() );
3381 if ( ( elapsed
> mViewItemJobStepChunkTimeout
) || ( elapsed
< 0 ) )
3383 return ViewItemJobInterrupted
;
3384 } // else proceed with the next pass
3387 // This is *really* a BUG
3388 kWarning() << "ERROR: returned an invalid result";
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
;
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
3410 elapsed
= tStart
.msecsTo( QTime::currentTime() );
3411 if ( ( elapsed
> mViewItemJobStepChunkTimeout
) || ( elapsed
< 0 ) )
3413 return ViewItemJobInterrupted
;
3414 } // else proceed with the next pass
3417 // This is *really* a BUG
3418 kWarning() << "ERROR: returned an invalid result";
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
;
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
3442 elapsed
= tStart
.msecsTo( QTime::currentTime() );
3443 if ( ( elapsed
> mViewItemJobStepChunkTimeout
) || ( elapsed
< 0 ) )
3445 return ViewItemJobInterrupted
;
3446 } // else proceed with the next pass
3449 // This is *really* a BUG
3450 kWarning() << "ERROR: returned an invalid result";
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
;
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
3476 elapsed
= tStart
.msecsTo( QTime::currentTime() );
3477 if ( ( elapsed
> mViewItemJobStepChunkTimeout
) || ( elapsed
< 0 ) )
3478 return ViewItemJobInterrupted
;
3479 // else proceed with the next pass
3482 // This is *really* a BUG
3483 kWarning() << "ERROR: returned an invalid result";
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
;
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
3507 elapsed
= tStart
.msecsTo( QTime::currentTime() );
3508 if ( ( elapsed
> mViewItemJobStepChunkTimeout
) || ( elapsed
< 0 ) )
3509 return ViewItemJobInterrupted
;
3510 // else proceed with the next pass
3513 // This is *really* a BUG
3514 kWarning() << "ERROR: returned an invalid result";
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
;
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
3538 elapsed
= tStart
.msecsTo( QTime::currentTime() );
3539 if ( ( elapsed
> mViewItemJobStepChunkTimeout
) || ( elapsed
< 0 ) )
3540 return ViewItemJobInterrupted
;
3541 // else proceed with the next pass
3544 // This is *really* a BUG
3545 kWarning() << "ERROR: returned an invalid result";;
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
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
;
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",
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()
3600 layoutChangeTime
= 0;
3601 expandingTreeTime
= 0;
3603 for ( int i
= 0; i
< numberOfPasses
; i
++ ) {
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();
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
;
3660 kDebug() << "Now follows a breakdown of the jobs.";
3662 for ( int i
= 0; i
< numberOfPasses
; i
++ ) {
3663 if ( totalTime
[i
] == 0 )
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
];
3677 kDebug() << "==========================================================";
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();
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();
3710 Stats::resetStats();
3711 Stats::totalMessages
= elements
;
3712 Stats::firstStartTime
.restart();
3715 Stats::numElements
[currentPass
] = elements
;
3716 Stats::currentJobStartTime
.restart();
3718 Stats::chunks
[currentPass
]++;
3719 Stats::lastPass
= currentPass
;
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.
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 ) );
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 ) );
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 ) );
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 ) );
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 ) );
3796 if( !job
->disconnectUI() )
3798 mView
->ignoreUpdateGeometries( false );
3799 // explicit call to updateGeometries() here
3800 mView
->updateGeometries();
3803 return ViewItemJobInterrupted
;
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();
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();
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();
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();
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
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
3875 // This is *really* a BUG
3876 kWarning() << "ERROR: returned an invalid result";
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();
3943 // not the first loading job
3946 // messages sorted by date
3947 ( mSortOrder
->messageSorting() == SortOrder::SortMessagesByDateTime
) ||
3948 ( mSortOrder
->messageSorting() == SortOrder::SortMessagesByDateTimeOfMostRecent
)
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.
3978 case ViewItemJobCompleted
:
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();
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
:
4008 case PreSelectFirstUnreadCentered
:
4009 bSelectionDone
= mView
->selectFirstMessageItem( MessageTypeUnreadOnly
, true ); // center
4011 case PreSelectOldestCentered
:
4012 mView
->setCurrentMessageItem( mOldestItem
, true /* center */ );
4013 bSelectionDone
= true;
4015 case PreSelectNewestCentered
:
4016 mView
->setCurrentMessageItem( mNewestItem
, true /* center */ );
4017 bSelectionDone
= true;
4020 // deal with selection below
4023 kWarning() << "ERROR: Unrecognized pre-selection mode " << (int)mPreSelectionMode
;
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
4048 // This is *really* a BUG
4049 kWarning() << "ERROR: returned an invalid result";
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 );
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 );
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.
4096 kDebug() << "Gonna restore current here" << mCurrentItemToRestoreAfterViewItemJobStep
->subject();
4097 mView
->setCurrentIndex( q
->index( mCurrentItemToRestoreAfterViewItemJobStep
, 0 ) );
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)
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
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 );
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() );
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:
4194 // | | -> shift up job
4196 // | | -> shift up job
4198 // | | -> shift up job
4204 // | | -> job unaffected
4208 // |-------------------------|---------|--------------|
4209 // 0 currentIndex endIndex count
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
);
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() );
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).
4259 ViewItemJob
* job
= mViewItemJobs
.at( jobCount
- 1 );
4260 if ( job
->currentPass() == ViewItemJob::Pass1Fill
)
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;
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:
4312 // | | -> shift down job
4314 // | | -> shift down and crop job
4318 // | | -> split job, crop and shift
4322 // | | -> job unaffected
4326 // |-------------------------|---------|--------------|
4327 // 0 currentIndex endIndex count
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.
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() );
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
);
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).
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).
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() );
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
4562 MessageItem
* Model::messageItemByStorageRow( int row
) const
4564 if ( !d
->mStorageModel
)
4566 ModelInvariantIndex
* idx
= d
->mInvariantRowMapper
->modelIndexRowToModelInvariantIndex( row
);
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
);
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
)
4599 d
->mPersistentSetManager
->removeSet( ref
);
4601 if ( d
->mPersistentSetManager
->setCount() < 1 )
4603 delete d
->mPersistentSetManager
;
4604 d
->mPersistentSetManager
= 0;
4608 #include "model.moc"