mirror of
https://bitbucket.org/smil3y/kde-playground.git
synced 2025-02-23 18:32:51 +00:00
486 lines
16 KiB
C++
486 lines
16 KiB
C++
/*
|
|
Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "storagemodel.h"
|
|
|
|
#include <messagecore/utils/stringutil.h>
|
|
#include <messagecore/settings/globalsettings.h>
|
|
#include <messagecore/helpers/nodehelper.h>
|
|
|
|
#include <akonadi/attributefactory.h>
|
|
#include <akonadi/collection.h>
|
|
#include <akonadi/collectionstatistics.h>
|
|
#include <akonadi/entitymimetypefiltermodel.h>
|
|
#include <akonadi/entitytreemodel.h>
|
|
#include <akonadi/item.h>
|
|
#include <akonadi/itemmodifyjob.h>
|
|
#include <akonadi/kmime/messagefolderattribute.h>
|
|
#include <akonadi/selectionproxymodel.h>
|
|
|
|
#include <KDE/KLocale>
|
|
#include "core/messageitem.h"
|
|
#include "core/settings.h"
|
|
#include "messagelistutil.h"
|
|
|
|
|
|
#include <QtCore/QAbstractItemModel>
|
|
#include <QtCore/QAtomicInt>
|
|
#include <QtCore/QScopedPointer>
|
|
#include <QItemSelectionModel>
|
|
#include <QtCore/QMimeData>
|
|
#include <QtCore/QCryptographicHash>
|
|
|
|
namespace MessageList
|
|
{
|
|
|
|
class StorageModel::Private
|
|
{
|
|
public:
|
|
void onSourceDataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight );
|
|
void onSelectionChanged();
|
|
void loadSettings();
|
|
|
|
StorageModel * const q;
|
|
|
|
QAbstractItemModel *mModel;
|
|
QAbstractItemModel *mChildrenFilterModel;
|
|
QItemSelectionModel *mSelectionModel;
|
|
|
|
Private( StorageModel *owner )
|
|
: q( owner ),
|
|
mModel(0),
|
|
mSelectionModel(0)
|
|
{}
|
|
};
|
|
|
|
} // namespace MessageList
|
|
|
|
using namespace Akonadi;
|
|
using namespace MessageList;
|
|
|
|
namespace {
|
|
|
|
KMime::Message::Ptr messageForItem( const Akonadi::Item &item )
|
|
{
|
|
if ( !item.hasPayload<KMime::Message::Ptr>() ) {
|
|
kWarning() << "Not a message" << item.id() << item.remoteId() << item.mimeType();
|
|
return KMime::Message::Ptr();
|
|
}
|
|
return item.payload<KMime::Message::Ptr>();
|
|
}
|
|
|
|
}
|
|
|
|
static QAtomicInt _k_attributeInitialized;
|
|
|
|
StorageModel::StorageModel( QAbstractItemModel *model, QItemSelectionModel *selectionModel, QObject *parent )
|
|
: Core::StorageModel( parent ), d( new Private( this ) )
|
|
{
|
|
d->mSelectionModel = selectionModel;
|
|
if ( _k_attributeInitialized.testAndSetAcquire( 0, 1 ) ) {
|
|
AttributeFactory::registerAttribute<MessageFolderAttribute>();
|
|
}
|
|
|
|
Akonadi::SelectionProxyModel *childrenFilter = new Akonadi::SelectionProxyModel( d->mSelectionModel, this );
|
|
childrenFilter->setSourceModel( model );
|
|
childrenFilter->setFilterBehavior( KSelectionProxyModel::ChildrenOfExactSelection );
|
|
d->mChildrenFilterModel = childrenFilter;
|
|
|
|
EntityMimeTypeFilterModel *itemFilter = new EntityMimeTypeFilterModel( this );
|
|
itemFilter->setSourceModel( childrenFilter );
|
|
itemFilter->addMimeTypeExclusionFilter( Collection::mimeType() );
|
|
itemFilter->addMimeTypeInclusionFilter( QLatin1String( "message/rfc822" ) );
|
|
itemFilter->setHeaderGroup( EntityTreeModel::ItemListHeaders );
|
|
|
|
d->mModel = itemFilter;
|
|
|
|
kDebug() << "Using model:" << model->metaObject()->className();
|
|
|
|
connect( d->mModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
|
|
this, SLOT(onSourceDataChanged(QModelIndex,QModelIndex)) );
|
|
|
|
connect( d->mModel, SIGNAL(layoutAboutToBeChanged()),
|
|
this, SIGNAL(layoutAboutToBeChanged()) );
|
|
connect( d->mModel, SIGNAL(layoutChanged()),
|
|
this, SIGNAL(layoutChanged()) );
|
|
connect( d->mModel, SIGNAL(modelAboutToBeReset()),
|
|
this, SIGNAL(modelAboutToBeReset()) );
|
|
connect( d->mModel, SIGNAL(modelReset()),
|
|
this, SIGNAL(modelReset()) );
|
|
|
|
//Here we assume we'll always get QModelIndex() in the parameters
|
|
connect( d->mModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
|
|
this, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)) );
|
|
connect( d->mModel, SIGNAL(rowsInserted(QModelIndex,int,int)),
|
|
this, SIGNAL(rowsInserted(QModelIndex,int,int)) );
|
|
connect( d->mModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
|
|
this, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)) );
|
|
connect( d->mModel, SIGNAL(rowsRemoved(QModelIndex,int,int)),
|
|
this, SIGNAL(rowsRemoved(QModelIndex,int,int)) );
|
|
|
|
connect( d->mSelectionModel, SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
|
|
this, SLOT(onSelectionChanged()) );
|
|
|
|
d->loadSettings();
|
|
connect( Core::Settings::self(), SIGNAL(configChanged()),
|
|
this, SLOT(loadSettings()) );
|
|
}
|
|
|
|
StorageModel::~StorageModel()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
Collection::List StorageModel::displayedCollections() const
|
|
{
|
|
Collection::List collections;
|
|
QModelIndexList indexes = d->mSelectionModel->selectedRows();
|
|
|
|
foreach ( const QModelIndex &index, indexes ) {
|
|
Collection c = index.data( EntityTreeModel::CollectionRole ).value<Collection>();
|
|
if ( c.isValid() ) {
|
|
collections << c;
|
|
}
|
|
}
|
|
|
|
return collections;
|
|
}
|
|
|
|
QString StorageModel::id() const
|
|
{
|
|
QStringList ids;
|
|
QModelIndexList indexes = d->mSelectionModel->selectedRows();
|
|
|
|
foreach ( const QModelIndex &index, indexes ) {
|
|
Collection c = index.data( EntityTreeModel::CollectionRole ).value<Collection>();
|
|
if ( c.isValid() ) {
|
|
ids << QString::number( c.id() );
|
|
}
|
|
}
|
|
|
|
ids.sort();
|
|
return ids.join(QLatin1String( ":" ));
|
|
}
|
|
|
|
bool StorageModel::isOutBoundFolder( const Akonadi::Collection& c ) const
|
|
{
|
|
if ( c.hasAttribute<MessageFolderAttribute>()
|
|
&& c.attribute<MessageFolderAttribute>()->isOutboundFolder() ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool StorageModel::containsOutboundMessages() const
|
|
{
|
|
QModelIndexList indexes = d->mSelectionModel->selectedRows();
|
|
|
|
foreach ( const QModelIndex &index, indexes ) {
|
|
Collection c = index.data( EntityTreeModel::CollectionRole ).value<Collection>();
|
|
if ( c.isValid() ) {
|
|
return isOutBoundFolder( c );
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int StorageModel::initialUnreadRowCountGuess() const
|
|
{
|
|
QModelIndexList indexes = d->mSelectionModel->selectedRows();
|
|
|
|
int unreadCount = 0;
|
|
|
|
foreach ( const QModelIndex &index, indexes ) {
|
|
Collection c = index.data( EntityTreeModel::CollectionRole ).value<Collection>();
|
|
if ( c.isValid() ) {
|
|
unreadCount+= c.statistics().unreadCount();
|
|
}
|
|
}
|
|
|
|
return unreadCount;
|
|
}
|
|
|
|
bool StorageModel::initializeMessageItem( MessageList::Core::MessageItem *mi,
|
|
int row, bool bUseReceiver ) const
|
|
{
|
|
const Item item = itemForRow( row );
|
|
const KMime::Message::Ptr mail = messageForItem( item );
|
|
if ( !mail ) {
|
|
return false;
|
|
}
|
|
|
|
const Collection parentCol = parentCollectionForRow( row );
|
|
|
|
QString sender;
|
|
if ( mail->from() ) {
|
|
sender = mail->from()->asUnicodeString();
|
|
}
|
|
QString receiver;
|
|
if ( mail->to() ) {
|
|
receiver = mail->to()->asUnicodeString();
|
|
}
|
|
|
|
// Static for speed reasons
|
|
static const QString noSubject = i18nc( "displayed as subject when the subject of a mail is empty", "No Subject" );
|
|
static const QString unknown( i18nc( "displayed when a mail has unknown sender, receiver or date", "Unknown" ) );
|
|
|
|
if ( sender.isEmpty() ) {
|
|
sender = unknown;
|
|
}
|
|
if ( receiver.isEmpty() ) {
|
|
receiver = unknown;
|
|
}
|
|
|
|
mi->initialSetup( mail->date()->dateTime().toTime_t(),
|
|
item.size(),
|
|
sender, receiver,
|
|
bUseReceiver );
|
|
mi->setItemId( item.id() );
|
|
mi->setParentCollectionId( parentCol.id() );
|
|
|
|
QString subject = mail->subject()->asUnicodeString();
|
|
if ( subject.isEmpty() ) {
|
|
subject = QLatin1Char( '(' ) + noSubject + QLatin1Char( ')' );
|
|
}
|
|
|
|
mi->setSubject( subject );
|
|
|
|
updateMessageItemData( mi, row );
|
|
|
|
return true;
|
|
}
|
|
|
|
static QByteArray md5Encode( const QByteArray &str )
|
|
{
|
|
if ( str.trimmed().isEmpty() ) return QByteArray();
|
|
|
|
QCryptographicHash c( QCryptographicHash::Md5 );
|
|
c.addData( str.trimmed() );
|
|
return c.result();
|
|
}
|
|
|
|
void StorageModel::fillMessageItemThreadingData( MessageList::Core::MessageItem *mi,
|
|
int row, ThreadingDataSubset subset ) const
|
|
{
|
|
const KMime::Message::Ptr mail = messageForRow( row );
|
|
Q_ASSERT( mail ); // We ASSUME that initializeMessageItem has been called successfully...
|
|
|
|
switch ( subset ) {
|
|
case PerfectThreadingReferencesAndSubject:
|
|
{
|
|
const QString subject = mail->subject()->asUnicodeString();
|
|
const QString strippedSubject = MessageCore::StringUtil::stripOffPrefixes( subject );
|
|
mi->setStrippedSubjectMD5( md5Encode( strippedSubject.toUtf8() ) );
|
|
mi->setSubjectIsPrefixed( subject != strippedSubject );
|
|
// fall through
|
|
}
|
|
case PerfectThreadingPlusReferences:
|
|
if ( !mail->references()->identifiers().isEmpty() ) {
|
|
mi->setReferencesIdMD5( md5Encode( mail->references()->identifiers().last() ) );
|
|
}
|
|
// fall through
|
|
case PerfectThreadingOnly:
|
|
mi->setMessageIdMD5( md5Encode( mail->messageID()->identifier() ) );
|
|
if ( !mail->inReplyTo()->identifiers().isEmpty() ) {
|
|
mi->setInReplyToIdMD5( md5Encode( mail->inReplyTo()->identifiers().first() ) );
|
|
}
|
|
break;
|
|
default:
|
|
Q_ASSERT( false ); // should never happen
|
|
break;
|
|
}
|
|
}
|
|
|
|
void StorageModel::updateMessageItemData( MessageList::Core::MessageItem *mi,
|
|
int row ) const
|
|
{
|
|
const Item item = itemForRow( row );
|
|
|
|
Akonadi::MessageStatus stat;
|
|
stat.setStatusFromFlags( item.flags() );
|
|
|
|
mi->setAkonadiItem( item );
|
|
mi->setStatus( stat );
|
|
|
|
|
|
if ( stat.isEncrypted() )
|
|
mi->setEncryptionState( Core::MessageItem::FullyEncrypted );
|
|
else
|
|
mi->setEncryptionState( Core::MessageItem::EncryptionStateUnknown );
|
|
|
|
|
|
if ( stat.isSigned() )
|
|
mi->setSignatureState( Core::MessageItem::FullySigned );
|
|
else
|
|
mi->setSignatureState( Core::MessageItem::SignatureStateUnknown );
|
|
|
|
mi->invalidateTagCache();
|
|
mi->invalidateAnnotationCache();
|
|
}
|
|
|
|
void StorageModel::setMessageItemStatus( MessageList::Core::MessageItem *mi,
|
|
int row, const Akonadi::MessageStatus &status )
|
|
{
|
|
Q_UNUSED( mi );
|
|
Item item = itemForRow( row );
|
|
item.setFlags( status.statusFlags() );
|
|
ItemModifyJob *job = new ItemModifyJob( item, this );
|
|
job->disableRevisionCheck();
|
|
job->setIgnorePayload( true );
|
|
}
|
|
|
|
QVariant StorageModel::data( const QModelIndex &index, int role ) const
|
|
{
|
|
// We don't provide an implementation for data() in No-Akonadi-KMail.
|
|
// This is because StorageModel must be a wrapper anyway (because columns
|
|
// must be re-mapped and functions not available in a QAbstractItemModel
|
|
// are strictly needed. So when porting to Akonadi this class will
|
|
// either wrap or subclass the MessageModel and implement initializeMessageItem()
|
|
// with appropriate calls to data(). And for No-Akonadi-KMail we still have
|
|
// a somewhat efficient implementation.
|
|
|
|
Q_UNUSED( index );
|
|
Q_UNUSED( role );
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
int StorageModel::columnCount( const QModelIndex &parent ) const
|
|
{
|
|
if ( !parent.isValid() )
|
|
return 1;
|
|
return 0; // this model is flat.
|
|
}
|
|
|
|
QModelIndex StorageModel::index( int row, int column, const QModelIndex &parent ) const
|
|
{
|
|
if ( !parent.isValid() )
|
|
return createIndex( row, column, 0 );
|
|
|
|
return QModelIndex(); // this model is flat.
|
|
}
|
|
|
|
QModelIndex StorageModel::parent( const QModelIndex &index ) const
|
|
{
|
|
Q_UNUSED( index );
|
|
return QModelIndex(); // this model is flat.
|
|
}
|
|
|
|
int StorageModel::rowCount( const QModelIndex &parent ) const
|
|
{
|
|
if ( !parent.isValid() )
|
|
return d->mModel->rowCount();
|
|
return 0; // this model is flat.
|
|
}
|
|
|
|
QMimeData* StorageModel::mimeData( const QList< MessageList::Core::MessageItem* >& items ) const
|
|
{
|
|
QMimeData *data = new QMimeData();
|
|
KUrl::List urls;
|
|
foreach ( MessageList::Core::MessageItem* mi, items ) {
|
|
Akonadi::Item item = itemForRow( mi->currentModelIndexRow() );
|
|
urls << item.url( Item::UrlWithMimeType );
|
|
}
|
|
|
|
urls.populateMimeData( data );
|
|
|
|
return data;
|
|
}
|
|
|
|
|
|
void StorageModel::prepareForScan()
|
|
{
|
|
|
|
}
|
|
|
|
void StorageModel::Private::onSourceDataChanged( const QModelIndex &topLeft, const QModelIndex &bottomRight )
|
|
{
|
|
emit q->dataChanged( q->index( topLeft.row(), 0 ),
|
|
q->index( bottomRight.row(), 0 ) );
|
|
}
|
|
|
|
void StorageModel::Private::onSelectionChanged()
|
|
{
|
|
emit q->headerDataChanged( Qt::Horizontal, 0, q->columnCount()-1 );
|
|
}
|
|
|
|
void StorageModel::Private::loadSettings()
|
|
{
|
|
// Custom/System colors
|
|
Core::Settings *settings = Core::Settings::self();
|
|
|
|
if ( MessageCore::GlobalSettings::self()->useDefaultColors() ) {
|
|
Core::MessageItem::setUnreadMessageColor( MessageList::Util::unreadDefaultMessageColor() );
|
|
Core::MessageItem::setImportantMessageColor( MessageList::Util::importantDefaultMessageColor() );
|
|
Core::MessageItem::setToDoMessageColor( MessageList::Util::todoDefaultMessageColor() );
|
|
} else {
|
|
Core::MessageItem::setUnreadMessageColor( settings->unreadMessageColor() );
|
|
Core::MessageItem::setImportantMessageColor( settings->importantMessageColor() );
|
|
Core::MessageItem::setToDoMessageColor( settings->todoMessageColor() );
|
|
}
|
|
|
|
if ( MessageCore::GlobalSettings::self()->useDefaultFonts() ) {
|
|
Core::MessageItem::setGeneralFont( KGlobalSettings::generalFont() );
|
|
Core::MessageItem::setUnreadMessageFont( KGlobalSettings::generalFont() );
|
|
Core::MessageItem::setImportantMessageFont( KGlobalSettings::generalFont() );
|
|
Core::MessageItem::setToDoMessageFont( KGlobalSettings::generalFont() );
|
|
} else {
|
|
Core::MessageItem::setGeneralFont( settings->messageListFont() );
|
|
Core::MessageItem::setUnreadMessageFont( settings->unreadMessageFont() );
|
|
Core::MessageItem::setImportantMessageFont( settings->importantMessageFont() );
|
|
Core::MessageItem::setToDoMessageFont( settings->todoMessageFont() );
|
|
}
|
|
}
|
|
|
|
Item StorageModel::itemForRow( int row ) const
|
|
{
|
|
return d->mModel->data( d->mModel->index( row, 0 ), EntityTreeModel::ItemRole ).value<Item>();
|
|
}
|
|
|
|
KMime::Message::Ptr StorageModel::messageForRow( int row ) const
|
|
{
|
|
return messageForItem( itemForRow( row ) );
|
|
}
|
|
|
|
Collection StorageModel::parentCollectionForRow(int row) const
|
|
{
|
|
QAbstractProxyModel *mimeProxy = static_cast<QAbstractProxyModel*>( d->mModel );
|
|
// This is index mapped to Akonadi::SelectionProxyModel
|
|
const QModelIndex childrenFilterIndex = mimeProxy->mapToSource( d->mModel->index( row, 0 ) );
|
|
Q_ASSERT( childrenFilterIndex.isValid() );
|
|
|
|
QAbstractProxyModel *childrenProxy = static_cast<QAbstractProxyModel*>( d->mChildrenFilterModel );
|
|
// This is index mapped to ETM
|
|
const QModelIndex etmIndex = childrenProxy->mapToSource( childrenFilterIndex );
|
|
Q_ASSERT( etmIndex.isValid() );
|
|
// We cannot possibly refer to top-level collection
|
|
Q_ASSERT( etmIndex.parent().isValid() );
|
|
|
|
const Collection col = etmIndex.parent().data( EntityTreeModel::CollectionRole ).value<Collection>();
|
|
Q_ASSERT( col.isValid() );
|
|
|
|
return col;
|
|
}
|
|
|
|
void StorageModel::resetModelStorage()
|
|
{
|
|
reset();
|
|
}
|
|
|
|
#include "moc_storagemodel.cpp"
|