2
Copyright (C) 2010 Klarälvdalens Datakonsult AB,
3
a KDAB Group company, info@kdab.net,
4
author Stephen Kelly <stephen@kdab.com>
6
This library is free software; you can redistribute it and/or modify it
7
under the terms of the GNU Library General Public License as published by
8
the Free Software Foundation; either version 2 of the License, or (at your
9
option) any later version.
11
This library is distributed in the hope that it will be useful, but WITHOUT
12
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
14
License for more details.
16
You should have received a copy of the GNU Library General Public License
17
along with this library; see the file COPYING.LIB. If not, write to the
18
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22
#include "threadmodel.h"
24
#include "rangemanager_p.h"
25
#include "threadgroupermodel.h"
27
#include <akonadi/kmime/messagestatus.h>
28
#include <kmime/kmime_message.h>
30
class ThreadModelPrivate
32
ThreadModelPrivate( QAbstractItemModel *emailModel, ThreadModel *qq )
33
: q_ptr( qq ), m_emailModel( emailModel )
37
Q_DECLARE_PUBLIC( ThreadModel )
38
ThreadModel* const q_ptr;
40
void slotRowsInserted( const QModelIndex&, int, int );
41
void slotRowsRemoved( const QModelIndex&, int, int );
42
void slotResetModel();
43
void populateThreadModel();
45
QAbstractItemModel *m_emailModel;
46
RangeManager m_rangeManager;
49
void ThreadModelPrivate::slotRowsInserted( const QModelIndex&, int start, int end )
52
* At this point the m_emailModel contains the new rows already, but the range manager
56
* The m_emailModel contained two threads each with two messages
58
* so the range manager contains the same information
60
* Now 4 new rows are inserted into the m_emailModel with one new message belonging to t1,
61
* one new message belonging to t2 and 2 new messages that are thread leaders themself.
62
* The m_emailModel will look like the following now
63
* EM[t1 t1 t1 t2 t3 t4 t4 t4]
64
* and we have to adapt the range manager to match this.
67
// At first find out if there exists a row before 'start' and what its thread id and associated range is
68
Akonadi::Item::Id previousThreadId = -1;
70
const QModelIndex index = m_emailModel->index( start - 1, 0 );
71
Q_ASSERT( index.isValid() );
72
previousThreadId = index.data( ThreadGrouperModel::ThreadIdRole ).toLongLong();
75
// check if there exists a row after 'end' and what its thread id and associated range is
76
Akonadi::Item::Id nextThreadId = -1;
77
if ( end != (m_emailModel->rowCount() - 1) ) {
78
const QModelIndex index = m_emailModel->index( end + 1, 0 );
79
Q_ASSERT( index.isValid() );
80
nextThreadId = index.data( ThreadGrouperModel::ThreadIdRole ).toLongLong();
83
// iterate over the new rows
84
Akonadi::Item::Id currentThreadId = previousThreadId;
85
int currentRange = (start == 0 ? -1 : m_rangeManager.rangeForPosition( start - 1 ));
87
for ( int row = start; row <= end; ++row ) {
88
const QModelIndex index = m_emailModel->index( row, 0 );
89
Q_ASSERT( index.isValid() );
91
const Akonadi::Item::Id threadRootId = index.data( ThreadGrouperModel::ThreadIdRole ).toLongLong();
92
if ( threadRootId == currentThreadId ) { // belongs to the current thread
93
m_rangeManager.increaseRange( currentRange, 1 );
95
const QModelIndex threadIndex = q_ptr->index( currentRange, 0 ); //TODO: cache it
96
emit q_ptr->dataChanged( threadIndex, threadIndex );
97
} else { // threadRootId != currentThreadId
98
if ( (row == end) && (threadRootId == nextThreadId) ) {
99
const int nextRange = currentRange + 1;
100
m_rangeManager.increaseRange( nextRange, 1 );
102
const QModelIndex threadIndex = q_ptr->index( nextRange, 0 );
103
emit q_ptr->dataChanged( threadIndex, threadIndex );
106
q_ptr->beginInsertRows( QModelIndex(), currentRange, currentRange );
107
m_rangeManager.insertRange( currentRange, 1 );
108
q_ptr->endInsertRows();
112
currentThreadId = threadRootId;
116
void ThreadModelPrivate::slotRowsRemoved( const QModelIndex&, int start, int end )
118
const int startRange = m_rangeManager.rangeForPosition( start );
119
const int endRange = m_rangeManager.rangeForPosition( end );
121
const int startRangeSize = m_rangeManager.rangeSize( startRange );
122
const int endRangeSize = m_rangeManager.rangeSize( endRange );
124
if ( startRange == endRange ) {
125
// the rows to be removed are covered by one range
127
const int rowCount = (end - start + 1);
128
if ( (rowCount - startRangeSize) == 0 ) { // all messages of this thread are removed -> remove the thread
129
q_ptr->beginRemoveRows( QModelIndex(), startRange, startRange );
130
m_rangeManager.removeRange( startRange );
131
q_ptr->endRemoveRows();
132
} else { // some messages are left in the thread -> adapt thread size
133
m_rangeManager.decreaseRange( startRange, rowCount );
135
const QModelIndex index = q_ptr->index( startRange, 0 );
136
emit q_ptr->dataChanged( index, index ); // the number of thread childs has changed -> trigger view update
139
// the rows to be removed are covered by two or more ranges
141
// first check how many rows of the start range are affected
142
const int startRangeStart = m_rangeManager.rangeStart( startRange + 1 );
143
const int affectedStartRows = (startRangeStart - start);
145
// check how many rows of the end range are affected
146
const int endRangeStart = m_rangeManager.rangeStart( endRange );
147
const int affectedEndRows = (end - endRangeStart + 1);
149
// we can't delete the ranges one by one, but have to remove them in one go,
150
// so store which is the first and last range to be deleted
151
int startRangeToDelete = startRange + 1;
152
int endRangeToDelete = endRange - 1;
154
// we can't update the indexes before the rows are removed, so delay it
155
bool updateStartRange = false;
156
bool updateEndRange = false;
158
if ( (affectedStartRows - startRangeSize) == 0 ) { // all messages of this thread are removed -> remove the thread
159
startRangeToDelete = startRange;
160
} else { // some messages are left in the thread -> adapt thread size
161
updateStartRange = true;
164
if ( (affectedEndRows - endRangeSize) == 0 ) { // all messages of this thread are removed -> remove the thread
165
endRangeToDelete = endRange;
166
} else { // some messages are left in the thread -> adapt thread size
167
updateEndRange = true;
170
// check if there are ranges that must be removed
171
if ( (endRangeToDelete - startRangeToDelete) >= 0 ) {
172
q_ptr->beginRemoveRows( QModelIndex(), startRangeToDelete, endRangeToDelete );
173
for ( int range = startRangeToDelete; range <= endRangeToDelete; ++range ) {
174
m_rangeManager.removeRange( startRangeToDelete );
176
q_ptr->endRemoveRows();
179
// no update the start range and end range as well
180
if ( updateStartRange ) {
181
m_rangeManager.decreaseRange( startRange, affectedStartRows );
183
const QModelIndex index = q_ptr->index( startRange, 0 );
184
emit q_ptr->dataChanged( index, index ); // the number of thread childs has changed -> trigger view update
187
if ( updateEndRange ) {
188
// we need to update the end range here, since the ranges between start and end have been removed already
189
const int updatedEndRange = (endRange - (endRangeToDelete - startRangeToDelete) - 1);
190
m_rangeManager.decreaseRange( updatedEndRange, affectedEndRows );
192
const QModelIndex index = q_ptr->index( updatedEndRange, 0 );
193
emit q_ptr->dataChanged( index, index ); // the number of thread childs has changed -> trigger view update
198
void ThreadModelPrivate::slotResetModel()
200
populateThreadModel();
203
void ThreadModelPrivate::populateThreadModel()
206
q->beginResetModel();
208
m_rangeManager.clear();
209
const int rowCount = m_emailModel->rowCount();
210
if ( rowCount == 0 ) {
215
const QModelIndex firstIndex = m_emailModel->index( 0, 0 );
216
Akonadi::Item::Id currentThreadId = firstIndex.data( ThreadGrouperModel::ThreadIdRole ).toLongLong();
219
static const int column = 0;
220
for ( int row = 1; row < rowCount; ++row ) {
221
const QModelIndex index = m_emailModel->index( row, column );
222
Q_ASSERT( index.isValid() );
224
const Akonadi::Item::Id threadRoot = index.data( ThreadGrouperModel::ThreadIdRole ).toLongLong();
225
if ( threadRoot != currentThreadId ) {
226
m_rangeManager.insertRange( m_rangeManager.count(), row - startRow );
230
currentThreadId = threadRoot;
233
m_rangeManager.insertRange( m_rangeManager.count(), rowCount - startRow );
237
ThreadModel::ThreadModel( QAbstractItemModel *emailModel, QObject *parent )
238
: QAbstractListModel( parent ), d_ptr( new ThreadModelPrivate( emailModel, this ) )
240
connect( emailModel, SIGNAL( rowsInserted( QModelIndex, int, int ) ),
241
this, SLOT( slotRowsInserted( QModelIndex, int, int ) ) );
243
connect( emailModel, SIGNAL( rowsRemoved( QModelIndex, int, int ) ),
244
this, SLOT( slotRowsRemoved( QModelIndex, int, int ) ) );
246
connect( emailModel, SIGNAL( layoutChanged() ),
247
this, SLOT( slotResetModel() ) );
249
connect( emailModel, SIGNAL( modelReset() ),
250
this, SLOT( slotResetModel() ) );
252
QHash<int, QByteArray> roleNames = emailModel->roleNames();
253
roleNames.insert( ThreadSizeRole, "threadSize" );
254
roleNames.insert( ThreadUnreadCountRole, "threadUnreadCount" );
255
setRoleNames( roleNames );
258
ThreadModel::~ThreadModel()
263
QVariant ThreadModel::data( const QModelIndex &index, int role ) const
265
Q_D( const ThreadModel );
267
if ( !index.isValid() )
270
const int indexRow = index.row();
271
if ( indexRow < d->m_rangeManager.count() ) {
272
const int range = indexRow;
274
const int rangeStartRow = d->m_rangeManager.rangeStart( range );
275
const int rangeSize = d->m_rangeManager.rangeSize( range );
276
const QModelIndex firstMailIndex = d->m_emailModel->index( rangeStartRow, 0 );
277
Q_ASSERT( firstMailIndex.isValid() );
279
if ( role == ThreadRangeStartRole )
280
return rangeStartRow;
281
if ( role == ThreadRangeEndRole )
282
return (rangeStartRow + rangeSize - 1);
283
if ( role == ThreadSizeRole )
285
if ( role == ThreadUnreadCountRole ) {
287
for ( int row = rangeStartRow; row <= (rangeStartRow + rangeSize - 1); ++row ) {
288
static const int column = 0;
290
const QModelIndex index = d->m_emailModel->index( row, column );
291
Q_ASSERT( index.isValid() );
293
const Akonadi::Item item = index.data( Akonadi::EntityTreeModel::ItemRole ).value<Akonadi::Item>();
294
Q_ASSERT( item.isValid() );
295
Q_ASSERT( item.hasPayload<KMime::Message::Ptr>() );
297
const KMime::Message::Ptr message = item.payload<KMime::Message::Ptr>();
298
Akonadi::MessageStatus status;
299
status.setStatusFromFlags( item.flags() );
300
if ( !status.isRead() )
307
if ( role == Qt::DisplayRole ) {
308
const QString displayString = firstMailIndex.data( role ).toString();
309
return QString("(" + QString::number( rangeSize ) + ")" + displayString);
312
return firstMailIndex.data( role );
318
int ThreadModel::rowCount( const QModelIndex& ) const
320
Q_D( const ThreadModel );
322
return d->m_rangeManager.count();
325
#include "threadmodel.moc"