mirror of
https://bitbucket.org/smil3y/kde-playground.git
synced 2025-02-24 19:02:51 +00:00
393 lines
15 KiB
C++
393 lines
15 KiB
C++
/*
|
|
Copyright (c) 2007 Bruno Virlet <bruno.virlet@gmail.com>
|
|
|
|
This library is free software; you can redistribute it and/or modify it
|
|
under the terms of the GNU Library General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or (at your
|
|
option) any later version.
|
|
|
|
This library 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 Library General Public
|
|
License for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; see the file COPYING.LIB. If not, write to the
|
|
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
02110-1301, USA.
|
|
*/
|
|
|
|
#include "messagethreaderproxymodel.h"
|
|
#include "messagethreadingattribute.h"
|
|
#include "messagemodel.h"
|
|
|
|
#include <akonadi/attributefactory.h>
|
|
#include <akonadi/itemfetchjob.h>
|
|
#include <akonadi/itemfetchscope.h>
|
|
|
|
#include <QtCore/QDebug>
|
|
#include <QtCore/QString>
|
|
#include <QtCore/QStringList>
|
|
#include <QtCore/QHash>
|
|
#include <QtCore/QTime>
|
|
#include <QtCore/QModelIndex>
|
|
|
|
using namespace Akonadi;
|
|
|
|
class MessageThreaderProxyModel::Private
|
|
{
|
|
public:
|
|
Private(MessageThreaderProxyModel *parent)
|
|
: mParent(parent)
|
|
{
|
|
}
|
|
|
|
MessageModel *sourceMessageModel()
|
|
{
|
|
return dynamic_cast<MessageModel *>(mParent->sourceModel());
|
|
}
|
|
|
|
/*
|
|
* Reset everything
|
|
*/
|
|
void slotCollectionChanged()
|
|
{
|
|
childrenMap.clear();
|
|
indexMap.clear();
|
|
parentMap.clear();
|
|
realPerfectParentsMap.clear();
|
|
realUnperfectParentsMap.clear();
|
|
realSubjectParentsMap.clear();
|
|
|
|
realPerfectChildrenMap.clear();
|
|
realUnperfectChildrenMap.clear();
|
|
realSubjectChildrenMap.clear();
|
|
|
|
mParent->reset();
|
|
}
|
|
|
|
/*
|
|
* Function called when the signal rowsInserted was triggered in the
|
|
* source model.
|
|
*/
|
|
void slotInsertRows(const QModelIndex &sourceIndex, int begin, int end)
|
|
{
|
|
Q_UNUSED(sourceIndex); // parent source index is always invalid (flat source model)
|
|
QTime time;
|
|
time.start();
|
|
|
|
for (int i = begin; i <= end; i++) {
|
|
// Retrieve the item from the source model
|
|
Item item = sourceMessageModel()->itemForIndex(sourceMessageModel()->index(i, 0));
|
|
Entity::Id id = item.id();
|
|
// Get his best potential parent using the mail threader parts
|
|
readParentsFromParts(item);
|
|
Entity::Id parentId = parentForItem(item.id());
|
|
|
|
/*
|
|
* Fill in the tree maps
|
|
*/
|
|
int row = childrenMap[parentId].count();
|
|
mParent->beginInsertRows(indexMap[parentId], row, row);
|
|
childrenMap[parentId] << item.id();
|
|
parentMap[id] = parentId;
|
|
QModelIndex index = mParent->createIndex(childrenMap[parentId].count() - 1, 0, id);
|
|
mParent->endInsertRows();
|
|
|
|
/*
|
|
* Look for potential children into real children map
|
|
*/
|
|
QList<Entity::Id> potentialChildren = realPerfectChildrenMap[id]
|
|
<< realUnperfectChildrenMap[id]
|
|
<< realSubjectChildrenMap[id];
|
|
foreach (Entity::Id potentialChildId, potentialChildren) {
|
|
// This item can be a child of our item if:
|
|
// - it's not the item itself (could we do that check when building the 'real' maps ?)
|
|
// - his parent is set
|
|
// - and this parent is not already our item
|
|
if (potentialChildId != id &&
|
|
parentMap.constFind(potentialChildId) != parentMap.constEnd() &&
|
|
parentMap[potentialChildId] != id &&
|
|
parentMap[potentialChildId]) {
|
|
// Check that the current parent of this item is not better than ours
|
|
QList<Entity::Id> realParentsList = realPerfectParentsMap[potentialChildId]
|
|
<< realUnperfectParentsMap[potentialChildId]
|
|
<< realSubjectParentsMap[potentialChildId];
|
|
int currentParentPos = realParentsList.indexOf(parentMap[potentialChildId]);
|
|
// currentParentPos = 0 is probably the more common case so we may avoid an indexOf.
|
|
if (currentParentPos == 0 || (currentParentPos != -1 && realParentsList.indexOf(id) > currentParentPos)) {
|
|
// (currentParentPos can be -1 if parent is root)
|
|
continue;
|
|
}
|
|
|
|
// Remove the children from the old location
|
|
int childRow = childrenMap[parentMap[potentialChildId]].indexOf(potentialChildId);
|
|
mParent->beginRemoveRows(indexMap[parentMap[potentialChildId]], childRow, childRow);
|
|
mParent->endRemoveRows();
|
|
childrenMap[parentMap[potentialChildId]].removeAt(childRow);
|
|
|
|
// Change the tree info
|
|
mParent->beginInsertRows(index, childrenMap[id].count(), childrenMap[id].count());
|
|
parentMap[potentialChildId] = id;
|
|
childrenMap[id] << potentialChildId;
|
|
|
|
// Recreate index because row change
|
|
mParent->createIndex(childrenMap[id].count() - 1, 0, potentialChildId);
|
|
mParent->endInsertRows();
|
|
}
|
|
}
|
|
}
|
|
|
|
qDebug() << time.elapsed() << "ms for" << end - begin + 1 << "items";
|
|
}
|
|
|
|
/*
|
|
* Function called when the signal rowsAboutToBeRemoved is sent by the source model
|
|
* (source model indexes are *still* valid)
|
|
*/
|
|
void slotRemoveRows(const QModelIndex &sourceIndex, int begin, int end)
|
|
{
|
|
Q_UNUSED(sourceIndex);
|
|
for (int i = begin; i <= end; i++) {
|
|
Item item = sourceMessageModel()->itemForIndex(sourceMessageModel()->index(i, 0));
|
|
Entity::Id id = item.id();
|
|
Entity::Id parentId = parentMap[id];
|
|
int row = childrenMap[parentId].indexOf(id);
|
|
|
|
// Reparent the children to the closest parent
|
|
foreach (Entity::Id childId, childrenMap[id]) {
|
|
int childRow = childrenMap[id].indexOf(childId);
|
|
mParent->beginRemoveRows(indexMap[id], childRow, childRow);
|
|
childrenMap[id].removeAll(childId); // There is only one ...
|
|
mParent->endRemoveRows();
|
|
|
|
mParent->beginInsertRows(indexMap[parentId], childrenMap[parentId].count(),
|
|
childrenMap[parentId].count());
|
|
parentMap[childId] = parentId;
|
|
childrenMap[parentId] << childId;
|
|
mParent->endInsertRows();
|
|
|
|
mParent->createIndex(childrenMap[parentId].count() - 1, 0, childId); // Is it necessary to recreate the index ?
|
|
}
|
|
|
|
mParent->beginRemoveRows(indexMap[parentId], row, row);
|
|
childrenMap[parentId].removeAll(id); // Remove this id from the children of parentId
|
|
parentMap.remove(id);
|
|
indexMap.remove(id);
|
|
mParent->endRemoveRows();
|
|
// mParent->beginRemoveColumns( indexMap[parentId], 0, sourceMessageModel()->columnCount() - 1 );
|
|
// mParent->endRemoveColumns();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This item has his parents stored in his threading parts.
|
|
* Read them and store them in the 'real' maps.
|
|
*
|
|
* We store both relationships :
|
|
* - child -> parents ( real*ParentsMap )
|
|
* - parent -> children ( real*ChildrenMap )
|
|
*/
|
|
void readParentsFromParts(const Item &item)
|
|
{
|
|
MessageThreadingAttribute *attr = item.attribute<MessageThreadingAttribute>();
|
|
if (attr) {
|
|
QList<Entity::Id> realPerfectParentsList = attr->perfectParents();
|
|
QList<Entity::Id> realUnperfectParentsList = attr->unperfectParents();
|
|
QList<Entity::Id> realSubjectParentsList = attr->subjectParents();
|
|
|
|
realPerfectParentsMap[item.id()] = realPerfectParentsList;
|
|
realUnperfectParentsMap[item.id()] = realUnperfectParentsList;
|
|
realSubjectParentsMap[item.id()] = realSubjectParentsList;
|
|
|
|
// Fill in the children maps
|
|
foreach (Entity::Id parentId, realPerfectParentsList) {
|
|
realPerfectChildrenMap[parentId] << item.id();
|
|
}
|
|
foreach (Entity::Id parentId, realUnperfectParentsList) {
|
|
realUnperfectChildrenMap[parentId] << item.id();
|
|
}
|
|
foreach (Entity::Id parentId, realSubjectParentsList) {
|
|
realSubjectChildrenMap[parentId] << item.id();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Find the first parent in the parents maps which is actually in the current collection
|
|
* @param id the item id
|
|
* @returns the parent id
|
|
*/
|
|
Entity::Id parentForItem(Entity::Id id)
|
|
{
|
|
|
|
QList<Entity::Id> parentsIds;
|
|
parentsIds << realPerfectParentsMap[id] << realUnperfectParentsMap[id] << realSubjectParentsMap[id];
|
|
|
|
foreach (Entity::Id parentId, parentsIds) {
|
|
// Check that the parent is in the collection
|
|
// This is time consuming but ... required.
|
|
if (sourceMessageModel()->indexForItem(Item(parentId), 0).isValid()) {
|
|
return parentId;
|
|
}
|
|
}
|
|
|
|
// TODO Check somewhere for 'parent loops' : in the parts, an item child of his child ...
|
|
return -1;
|
|
}
|
|
|
|
// -1 is an invalid id which means 'root'
|
|
Entity::Id idForIndex(const QModelIndex &index)
|
|
{
|
|
return index.isValid() ? index.internalId() : -1;
|
|
}
|
|
|
|
MessageThreaderProxyModel *mParent;
|
|
|
|
/*
|
|
* These maps store the current tree structure, as presented in the view.
|
|
* It tries to be as close as possible from the real structure, given that not every parents
|
|
* are present in the collection
|
|
*/
|
|
QHash<Entity::Id, QList<Entity::Id> > childrenMap;
|
|
QHash<Entity::Id, Entity::Id> parentMap;
|
|
QHash<Entity::Id, QModelIndex> indexMap;
|
|
|
|
/*
|
|
* These maps store the real parents, as read from the item parts
|
|
* In the best case, the list should contain only one element ( = unique parent )
|
|
* If there isn't only one, the algorithm will pick up the first one in the current collection
|
|
*/
|
|
QHash<Entity::Id, QList<Entity::Id> > realPerfectParentsMap;
|
|
QHash<Entity::Id, QList<Entity::Id> > realUnperfectParentsMap;
|
|
QHash<Entity::Id, QList<Entity::Id> > realSubjectParentsMap;
|
|
|
|
QHash<Entity::Id, QList<Entity::Id> > realPerfectChildrenMap;
|
|
QHash<Entity::Id, QList<Entity::Id> > realUnperfectChildrenMap;
|
|
QHash<Entity::Id, QList<Entity::Id> > realSubjectChildrenMap;
|
|
};
|
|
|
|
MessageThreaderProxyModel::MessageThreaderProxyModel(QObject *parent)
|
|
: QAbstractProxyModel(parent)
|
|
, d(new Private(this))
|
|
{
|
|
AttributeFactory::registerAttribute<MessageThreadingAttribute>();
|
|
}
|
|
|
|
MessageThreaderProxyModel::~MessageThreaderProxyModel()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
QModelIndex MessageThreaderProxyModel::index(int row, int column, const QModelIndex &parent) const
|
|
{
|
|
Entity::Id parentId = d->idForIndex(parent);
|
|
|
|
if (row < 0
|
|
|| column < 0
|
|
|| row >= d->childrenMap[parentId].count()
|
|
|| column >= columnCount(parent)) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
Entity::Id id = d->childrenMap[parentId].at(row);
|
|
|
|
return createIndex(row, column, id);
|
|
}
|
|
|
|
QModelIndex MessageThreaderProxyModel::parent(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid()) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
Entity::Id parentId = d->parentMap[index.internalId()];
|
|
|
|
if (parentId == -1) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
// int parentParentId = d->parentMap[parentId];
|
|
//int row = d->childrenMap[parentParentId].indexOf( parentId );
|
|
return d->indexMap[d->parentMap[index.internalId()]];
|
|
//return createIndex( row, 0, parentId );
|
|
}
|
|
|
|
QModelIndex MessageThreaderProxyModel::mapToSource(const QModelIndex &index) const
|
|
{
|
|
// This function is slow because it relies on rowForItem in the ItemModel (linear time)
|
|
return d->sourceMessageModel()->indexForItem(Item(index.internalId()), index.column());
|
|
}
|
|
|
|
QModelIndex MessageThreaderProxyModel::mapFromSource(const QModelIndex &index) const
|
|
{
|
|
Item item = d->sourceMessageModel()->itemForIndex(index);
|
|
Entity::Id id = item.id();
|
|
//return d->indexMap[id ]; // FIXME take column in account like mapToSource
|
|
return MessageThreaderProxyModel::index(d->indexMap[id].row(), index.column(), d->indexMap[id].parent());
|
|
}
|
|
|
|
QModelIndex MessageThreaderProxyModel::createIndex(int row, int column, quint32 internalId) const
|
|
{
|
|
QModelIndex index = QAbstractProxyModel::createIndex(row, column, internalId);
|
|
if (column == 0) {
|
|
d->indexMap[internalId] = index; // Store the newly created index in the index map
|
|
}
|
|
return index;
|
|
}
|
|
|
|
void MessageThreaderProxyModel::setSourceModel(QAbstractItemModel *model)
|
|
{
|
|
// TODO Assert model is a MessageModel
|
|
QAbstractProxyModel::setSourceModel(model);
|
|
|
|
d->sourceMessageModel()->fetchScope().fetchAttribute<MessageThreadingAttribute>();
|
|
|
|
// TODO disconnect old model
|
|
connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(slotInsertRows(QModelIndex,int,int)));
|
|
connect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), SLOT(slotRemoveRows(QModelIndex,int,int)));
|
|
connect(d->sourceMessageModel(), SIGNAL(collectionChanged(Akonadi::Collection)), SLOT(slotCollectionChanged()));
|
|
}
|
|
|
|
bool MessageThreaderProxyModel::hasChildren(const QModelIndex &index) const
|
|
{
|
|
return rowCount(index) > 0;
|
|
}
|
|
|
|
int MessageThreaderProxyModel::columnCount(const QModelIndex &index) const
|
|
{
|
|
Q_UNUSED(index);
|
|
// We assume that the source model has the same number of columns for each rows
|
|
return sourceModel()->columnCount(QModelIndex());
|
|
}
|
|
|
|
int MessageThreaderProxyModel::rowCount(const QModelIndex &index) const
|
|
{
|
|
Entity::Id id = d->idForIndex(index);
|
|
if (id == -1) {
|
|
return d->childrenMap[-1].count();
|
|
}
|
|
|
|
if (index.column() == 0) { // QModelIndex() has children
|
|
return d->childrenMap[id].count();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
QStringList MessageThreaderProxyModel::mimeTypes() const
|
|
{
|
|
return d->sourceMessageModel()->mimeTypes();
|
|
}
|
|
|
|
QMimeData *MessageThreaderProxyModel::mimeData(const QModelIndexList &indexes) const
|
|
{
|
|
QModelIndexList sourceIndexes;
|
|
for (int i = 0; i < indexes.count(); i++) {
|
|
sourceIndexes << mapToSource(indexes.at(i));
|
|
}
|
|
|
|
return sourceModel()->mimeData(sourceIndexes);
|
|
}
|
|
|
|
#include "moc_messagethreaderproxymodel.cpp"
|