mirror of
https://bitbucket.org/smil3y/kde-playground.git
synced 2025-02-24 02:42:51 +00:00
923 lines
29 KiB
C++
923 lines
29 KiB
C++
/*
|
|
Copyright (c) 2008 Thomas Thrainer <tom_t@gmx.at>
|
|
Copyright (c) 2012 Sérgio Martins <iamsergio@gmail.com>
|
|
|
|
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.
|
|
|
|
As a special exception, permission is given to link this program
|
|
with any edition of Qt, and distribute the resulting executable,
|
|
without including the source code for Qt in the source distribution.
|
|
*/
|
|
|
|
#include "todomodel_p.h"
|
|
#include "incidencetreemodel.h"
|
|
|
|
#include <calendarsupport/utils.h>
|
|
#include <calendarsupport/kcalprefs.h>
|
|
#include <KLocalizedString>
|
|
#include <KCalCore/Todo>
|
|
#include <KCalCore/Event>
|
|
#include <KCalCore/Attachment>
|
|
#include <KCalUtils/IncidenceFormatter>
|
|
#include <KPIMUtils/Email>
|
|
|
|
#include <KCalUtils/DndFactory>
|
|
#include <KCalUtils/ICalDrag>
|
|
#include <KCalUtils/VCalDrag>
|
|
|
|
#include <Akonadi/Calendar/ETMCalendar>
|
|
|
|
#include <KMessageBox>
|
|
|
|
#include <QMimeData>
|
|
#include <QIcon>
|
|
|
|
struct SourceModelIndex
|
|
{
|
|
SourceModelIndex( int _r, int _c, void *_p, QAbstractItemModel *_m )
|
|
: r(_r), c(_c), p(_p), m(_m)
|
|
{
|
|
}
|
|
|
|
operator QModelIndex()
|
|
{
|
|
return reinterpret_cast<QModelIndex &>( *this );
|
|
}
|
|
|
|
int r, c;
|
|
void *p;
|
|
const QAbstractItemModel *m;
|
|
};
|
|
|
|
static bool isDueToday( const KCalCore::Todo::Ptr &todo )
|
|
{
|
|
return !todo->isCompleted() && todo->dtDue().date() == QDate::currentDate();
|
|
}
|
|
|
|
TodoModel::Private::Private( const EventViews::PrefsPtr &preferences,
|
|
TodoModel *qq ) : QObject(), m_changer(0)
|
|
, m_preferences( preferences )
|
|
, q( qq )
|
|
{
|
|
}
|
|
|
|
Akonadi::Item TodoModel::Private::findItemByUid( const QString &uid,
|
|
const QModelIndex &parent ) const
|
|
{
|
|
Q_ASSERT( !uid.isEmpty() );
|
|
IncidenceTreeModel *treeModel = qobject_cast<IncidenceTreeModel*>( q->sourceModel() );
|
|
if ( treeModel ) { // O(1) Shortcut
|
|
return treeModel->item( uid );
|
|
}
|
|
|
|
Akonadi::Item item;
|
|
const int count = q->rowCount( parent );
|
|
for ( int i=0; i<count; ++i ) {
|
|
const QModelIndex currentIndex = q->index( i, 0, parent );
|
|
Q_ASSERT( currentIndex.isValid() );
|
|
item = q->data( currentIndex, Akonadi::EntityTreeModel::ItemRole ).value<Akonadi::Item>();
|
|
if ( item.isValid() ) {
|
|
return item;
|
|
} else {
|
|
item = findItemByUid( uid, currentIndex );
|
|
if ( item.isValid() ) {
|
|
return item;
|
|
}
|
|
}
|
|
}
|
|
|
|
return item;
|
|
}
|
|
|
|
void TodoModel::Private::onDataChanged( const QModelIndex &begin, const QModelIndex &end )
|
|
{
|
|
Q_ASSERT( begin.isValid() );
|
|
Q_ASSERT( end.isValid() );
|
|
const QModelIndex proxyBegin = q->mapFromSource( begin );
|
|
Q_ASSERT( proxyBegin.column() == 0 );
|
|
const QModelIndex proxyEnd = q->mapFromSource( end );
|
|
emit q->dataChanged( proxyBegin, proxyEnd.sibling( proxyEnd.row(), TodoModel::ColumnCount-1 ) );
|
|
}
|
|
|
|
void TodoModel::Private::onHeaderDataChanged( Qt::Orientation orientation, int first, int last )
|
|
{
|
|
emit q->headerDataChanged( orientation, first, last );
|
|
}
|
|
|
|
void TodoModel::Private::onRowsAboutToBeInserted( const QModelIndex &parent, int begin, int end )
|
|
{
|
|
const QModelIndex index = q->mapFromSource( parent );
|
|
Q_ASSERT( !( parent.isValid() ^ index.isValid() ) ); // Both must be valid, or both invalid
|
|
Q_ASSERT( !( index.isValid() && index.model() != q ) );
|
|
|
|
q->beginInsertRows( index, begin, end );
|
|
}
|
|
|
|
void TodoModel::Private::onRowsInserted( const QModelIndex &, int, int )
|
|
{
|
|
q->endInsertRows();
|
|
}
|
|
|
|
void TodoModel::Private::onRowsAboutToBeRemoved( const QModelIndex &parent, int begin, int end )
|
|
{
|
|
const QModelIndex index = q->mapFromSource( parent );
|
|
Q_ASSERT( !( parent.isValid() ^ index.isValid() ) ); // Both must be valid, or both invalid
|
|
Q_ASSERT( !( index.isValid() && index.model() != q ) );
|
|
|
|
q->beginRemoveRows( index, begin, end );
|
|
}
|
|
|
|
void TodoModel::Private::onRowsRemoved( const QModelIndex &, int, int )
|
|
{
|
|
q->endRemoveRows();
|
|
}
|
|
|
|
void TodoModel::Private::onRowsAboutToBeMoved( const QModelIndex &sourceParent,
|
|
int sourceStart,
|
|
int sourceEnd,
|
|
const QModelIndex &destinationParent,
|
|
int destinationRow )
|
|
{
|
|
Q_UNUSED( sourceParent );
|
|
Q_UNUSED( sourceStart );
|
|
Q_UNUSED( sourceEnd );
|
|
Q_UNUSED( destinationParent );
|
|
Q_UNUSED( destinationRow );
|
|
/* Disabled for now, layoutAboutToBeChanged() is emitted
|
|
q->beginMoveRows( q->mapFromSource( sourceParent ), sourceStart, sourceEnd,
|
|
q->mapFromSource( destinationParent ), destinationRow );
|
|
*/
|
|
}
|
|
|
|
void TodoModel::Private::onRowsMoved( const QModelIndex &, int, int, const QModelIndex &, int )
|
|
{
|
|
/*q->endMoveRows();*/
|
|
}
|
|
|
|
void TodoModel::Private::onModelAboutToBeReset()
|
|
{
|
|
q->beginResetModel();
|
|
}
|
|
|
|
void TodoModel::Private::onModelReset()
|
|
{
|
|
q->endResetModel();
|
|
}
|
|
|
|
void TodoModel::Private::onLayoutAboutToBeChanged()
|
|
{
|
|
Q_ASSERT( m_persistentIndexes.isEmpty() );
|
|
Q_ASSERT( m_layoutChangePersistentIndexes.isEmpty() );
|
|
Q_ASSERT( m_columns.isEmpty() );
|
|
QModelIndexList persistentIndexes = q->persistentIndexList();
|
|
foreach ( const QPersistentModelIndex &persistentIndex, persistentIndexes ) {
|
|
m_persistentIndexes << persistentIndex; // Stuff we have to update onLayoutChanged
|
|
Q_ASSERT( persistentIndex.isValid() );
|
|
QModelIndex index_col0 = q->createIndex( persistentIndex.row(), 0,
|
|
persistentIndex.internalPointer() );
|
|
const QPersistentModelIndex srcPersistentIndex = q->mapToSource( index_col0 );
|
|
Q_ASSERT( srcPersistentIndex.isValid() );
|
|
m_layoutChangePersistentIndexes << srcPersistentIndex;
|
|
m_columns << persistentIndex.column();
|
|
}
|
|
emit q->layoutAboutToBeChanged();
|
|
}
|
|
|
|
void TodoModel::Private::onLayoutChanged()
|
|
{
|
|
for ( int i = 0; i < m_persistentIndexes.size(); ++i ) {
|
|
QModelIndex newIndex_col0 = q->mapFromSource( m_layoutChangePersistentIndexes.at( i ) );
|
|
Q_ASSERT( newIndex_col0.isValid() );
|
|
const int column = m_columns.at( i );
|
|
QModelIndex newIndex =
|
|
column == 0 ?
|
|
newIndex_col0 :
|
|
q->createIndex( newIndex_col0.row(), column, newIndex_col0.internalPointer() );
|
|
q->changePersistentIndex( m_persistentIndexes.at( i ), newIndex );
|
|
}
|
|
|
|
m_layoutChangePersistentIndexes.clear();
|
|
m_persistentIndexes.clear();
|
|
m_columns.clear();
|
|
emit q->layoutChanged();
|
|
}
|
|
|
|
TodoModel::TodoModel( const EventViews::PrefsPtr &preferences, QObject *parent )
|
|
: QAbstractProxyModel( parent ), d( new Private( preferences, this ) )
|
|
{
|
|
setObjectName( QLatin1String("TodoModel") );
|
|
}
|
|
|
|
TodoModel::~TodoModel()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
QVariant TodoModel::data( const QModelIndex &index, int role ) const
|
|
{
|
|
Q_ASSERT( index.isValid() );
|
|
if ( !index.isValid() || !d->m_calendar ) {
|
|
return QVariant();
|
|
}
|
|
|
|
const QModelIndex sourceIndex = mapToSource( index.sibling( index.row(), 0 ) );
|
|
if ( !sourceIndex.isValid() ) {
|
|
return QVariant();
|
|
}
|
|
Q_ASSERT( sourceIndex.isValid() );
|
|
const Akonadi::Item item =
|
|
sourceIndex.data( Akonadi::EntityTreeModel::ItemRole ).value<Akonadi::Item>();
|
|
if ( !item.isValid() ) {
|
|
kWarning() << "Invalid index: " << sourceIndex;
|
|
//Q_ASSERT( false );
|
|
return QVariant();
|
|
}
|
|
const KCalCore::Todo::Ptr todo = CalendarSupport::todo( item );
|
|
if ( !todo ) {
|
|
kError() << "item.hasPayload()" << item.hasPayload();
|
|
if ( item.hasPayload<KCalCore::Incidence::Ptr>() ) {
|
|
KCalCore::Incidence::Ptr incidence = item.payload<KCalCore::Incidence::Ptr>();
|
|
if ( incidence )
|
|
kError() << "It's actually " << incidence->type();
|
|
}
|
|
|
|
Q_ASSERT(!"There's no to-do.");
|
|
return QVariant();
|
|
}
|
|
|
|
if ( role == Qt::DisplayRole ) {
|
|
switch ( index.column() ) {
|
|
case SummaryColumn:
|
|
return QVariant( todo->summary() );
|
|
case RecurColumn:
|
|
if (todo->recurs()) {
|
|
if (todo->hasRecurrenceId()) {
|
|
return i18nc( "yes, an exception to a recurring to-do", "Exception" );
|
|
} else {
|
|
return i18nc( "yes, recurring to-do", "Yes" );
|
|
}
|
|
} else {
|
|
return i18nc( "no, not a recurring to-do", "No" );
|
|
}
|
|
case PriorityColumn:
|
|
if ( todo->priority() == 0 ) {
|
|
return QVariant( QString::fromLatin1( "--" ) );
|
|
}
|
|
return QVariant( todo->priority() );
|
|
case PercentColumn:
|
|
return QVariant( todo->percentComplete() );
|
|
case StartDateColumn:
|
|
return todo->hasStartDate() ? QVariant( KCalUtils::IncidenceFormatter::dateToString( todo->dtStart() ) )
|
|
: QVariant( QString() );
|
|
case DueDateColumn:
|
|
return todo->hasDueDate() ? QVariant( KCalUtils::IncidenceFormatter::dateToString( todo->dtDue() ) )
|
|
: QVariant( QString() );
|
|
case CategoriesColumn:
|
|
{
|
|
QString categories = todo->categories().join(
|
|
i18nc( "delimiter for joining category names", "," ) );
|
|
return QVariant( categories );
|
|
}
|
|
case DescriptionColumn:
|
|
return QVariant( todo->description() );
|
|
case CalendarColumn:
|
|
return QVariant( CalendarSupport::displayName( d->m_calendar.data(), item.parentCollection() ) );
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
if ( role == Qt::EditRole ) {
|
|
switch ( index.column() ) {
|
|
case SummaryColumn:
|
|
return QVariant( todo->summary() );
|
|
case RecurColumn:
|
|
return QVariant( todo->recurs() );
|
|
case PriorityColumn:
|
|
return QVariant( todo->priority() );
|
|
case PercentColumn:
|
|
return QVariant( todo->percentComplete() );
|
|
case StartDateColumn:
|
|
return QVariant( todo->dtStart().date() );
|
|
case DueDateColumn:
|
|
return QVariant( todo->dtDue().date() );
|
|
case CategoriesColumn:
|
|
return QVariant( todo->categories() );
|
|
case DescriptionColumn:
|
|
return QVariant( todo->description() );
|
|
case CalendarColumn:
|
|
return QVariant( CalendarSupport::displayName( d->m_calendar.data(), item.parentCollection() ) );
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
// set the tooltip for every item
|
|
if ( role == Qt::ToolTipRole ) {
|
|
if ( d->m_preferences->enableToolTips() ) {
|
|
return QVariant( KCalUtils::IncidenceFormatter::toolTipStr(
|
|
CalendarSupport::displayName( d->m_calendar.data(), item.parentCollection() ),
|
|
todo, QDate(), true,
|
|
CalendarSupport::KCalPrefs::instance()->timeSpec() ) );
|
|
} else {
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
// background colour for todos due today or overdue todos
|
|
if ( role == Qt::BackgroundRole ) {
|
|
if ( todo->isOverdue() ) {
|
|
return QVariant(
|
|
QBrush( d->m_preferences->todoOverdueColor() ) );
|
|
} else if ( isDueToday( todo ) ) {
|
|
return QVariant(
|
|
QBrush( d->m_preferences->todoDueTodayColor() ) );
|
|
}
|
|
}
|
|
|
|
// indicate if a row is checked (=completed) only in the first column
|
|
if ( role == Qt::CheckStateRole && index.column() == 0 ) {
|
|
|
|
if (hasChildren(index) && !index.parent().isValid()) {
|
|
return QVariant();
|
|
}
|
|
|
|
if ( todo->isCompleted() ) {
|
|
return QVariant( Qt::Checked );
|
|
} else {
|
|
return QVariant( Qt::Unchecked );
|
|
}
|
|
}
|
|
|
|
// icon for recurring todos
|
|
// It's in the summary column so you don't accidentally click
|
|
// the checkbox ( which increments the next occurrence date ).
|
|
if ( role == Qt::DecorationRole && index.column() == SummaryColumn ) {
|
|
if ( todo->recurs() ) {
|
|
return QVariant( QIcon( SmallIcon( QLatin1String("task-recurring") ) ) );
|
|
}
|
|
}
|
|
|
|
// category colour
|
|
if ( role == Qt::DecorationRole && index.column() == SummaryColumn ) {
|
|
QStringList categories = todo->categories();
|
|
return categories.isEmpty() ?
|
|
QVariant() :
|
|
QVariant( CalendarSupport::KCalPrefs::instance()->categoryColor( categories.first() ) );
|
|
} else if ( role == Qt::DecorationRole ) {
|
|
return QVariant();
|
|
}
|
|
|
|
if ( role == TodoRole ) {
|
|
QVariant ret( QMetaType::VoidStar );
|
|
ret.setValue( item );
|
|
return ret;
|
|
}
|
|
|
|
if ( role == IsRichTextRole ) {
|
|
if ( index.column() == SummaryColumn ) {
|
|
return QVariant( todo->summaryIsRich() );
|
|
} else if ( index.column() == DescriptionColumn ) {
|
|
return QVariant( todo->descriptionIsRich() );
|
|
} else {
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
if ( role == Qt::TextAlignmentRole ) {
|
|
switch ( index.column() ) {
|
|
// If you change this, change headerData() too.
|
|
case RecurColumn:
|
|
case PriorityColumn:
|
|
case PercentColumn:
|
|
case StartDateColumn:
|
|
case DueDateColumn:
|
|
case CategoriesColumn:
|
|
case CalendarColumn:
|
|
return QVariant( Qt::AlignHCenter | Qt::AlignVCenter );
|
|
}
|
|
return QVariant( Qt::AlignLeft | Qt::AlignVCenter );
|
|
}
|
|
|
|
if ( sourceModel() ) {
|
|
return sourceModel()->data( mapToSource( index.sibling( index.row(), 0 ) ), role );
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
bool TodoModel::setData( const QModelIndex &index, const QVariant &value, int role )
|
|
{
|
|
Q_ASSERT( index.isValid() );
|
|
if ( !d->m_changer ) {
|
|
return false;
|
|
}
|
|
const QVariant oldValue = data( index, role );
|
|
|
|
if ( oldValue == value ) {
|
|
// Nothing changed, the user used one of the QStyledDelegate's editors but seted the old value
|
|
// Lets just skip this then and avoid a roundtrip to akonadi, and avoid sending invitations
|
|
return true;
|
|
}
|
|
|
|
const Akonadi::Item item =
|
|
data( index, Akonadi::EntityTreeModel::ItemRole ).value<Akonadi::Item>();
|
|
const KCalCore::Todo::Ptr todo = CalendarSupport::todo( item );
|
|
|
|
if ( !item.isValid() || !todo ) {
|
|
kWarning() << "TodoModel::setData() called, bug item is invalid or doesn't have payload";
|
|
Q_ASSERT( false );
|
|
return false;
|
|
}
|
|
|
|
if ( d->m_calendar->hasRight( item, Akonadi::Collection::CanChangeItem ) ) {
|
|
KCalCore::Todo::Ptr oldTodo( todo->clone() );
|
|
if ( role == Qt::CheckStateRole && index.column() == 0 ) {
|
|
const bool checked = static_cast<Qt::CheckState>( value.toInt() ) == Qt::Checked;
|
|
if ( checked )
|
|
todo->setCompleted( KDateTime::currentLocalDateTime() ); // Because it calls Todo::recurTodo()
|
|
else
|
|
todo->setCompleted( false );
|
|
}
|
|
|
|
if ( role == Qt::EditRole ) {
|
|
switch ( index.column() ) {
|
|
case SummaryColumn:
|
|
if ( !value.toString().isEmpty() ) {
|
|
todo->setSummary( value.toString() );
|
|
}
|
|
break;
|
|
case PriorityColumn:
|
|
todo->setPriority( value.toInt() );
|
|
break;
|
|
case PercentColumn:
|
|
todo->setPercentComplete( value.toInt() );
|
|
break;
|
|
case StartDateColumn:
|
|
{
|
|
KDateTime tmp = todo->dtStart();
|
|
tmp.setDate( value.toDate() );
|
|
todo->setDtStart( tmp );
|
|
}
|
|
break;
|
|
case DueDateColumn:
|
|
{
|
|
KDateTime tmp = todo->dtDue();
|
|
tmp.setDate( value.toDate() );
|
|
todo->setDtDue( tmp );
|
|
}
|
|
break;
|
|
case CategoriesColumn:
|
|
todo->setCategories( value.toStringList() );
|
|
break;
|
|
case DescriptionColumn:
|
|
todo->setDescription( value.toString() );
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( !todo->dirtyFields().isEmpty() ) {
|
|
d->m_changer->modifyIncidence( item, oldTodo );
|
|
// modifyIncidence will eventually call the view's
|
|
// changeIncidenceDisplay method, which in turn
|
|
// will call processChange. processChange will then emit
|
|
// dataChanged to the view, so we don't have to
|
|
// do it here
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
if ( !( role == Qt::CheckStateRole && index.column() == 0 ) ) {
|
|
//KOHelper::showSaveIncidenceErrorMsg( 0, todo ); //TODO pass parent
|
|
kError() << "Unable to modify incidence";
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int TodoModel::rowCount( const QModelIndex &parent ) const
|
|
{
|
|
if ( sourceModel() ) {
|
|
if ( parent.isValid() ) {
|
|
QModelIndex parent_col0 = createIndex( parent.row(), 0, parent.internalPointer() );
|
|
return sourceModel()->rowCount( mapToSource( parent_col0 ) );
|
|
} else {
|
|
return sourceModel()->rowCount();
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int TodoModel::columnCount( const QModelIndex & ) const
|
|
{
|
|
return ColumnCount;
|
|
}
|
|
|
|
void TodoModel::setSourceModel( QAbstractItemModel *model )
|
|
{
|
|
if ( model == sourceModel() ) {
|
|
return;
|
|
}
|
|
|
|
beginResetModel();
|
|
|
|
if ( sourceModel() ) {
|
|
disconnect( sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
|
|
d, SLOT(onDataChanged(QModelIndex,QModelIndex)) );
|
|
disconnect( sourceModel(), SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
|
|
d, SLOT(onHeaderDataChanged(Qt::Orientation,int,int)) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
|
|
d, SLOT(onRowsInserted(QModelIndex,int,int)) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
|
|
d, SLOT(onRowsRemoved(QModelIndex,int,int)) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
|
|
d, SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int)) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
|
|
d, SLOT(onRowsAboutToBeInserted(QModelIndex,int,int)) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
|
|
d, SLOT(onRowsAboutToBeRemoved(QModelIndex,int,int)) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(modelAboutToBeReset()),
|
|
d, SLOT(onModelAboutToBeReset()) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(modelReset()),
|
|
d, SLOT(onModelReset()) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(layoutAboutToBeChanged()),
|
|
d, SLOT(onLayoutAboutToBeChanged()) );
|
|
|
|
disconnect( sourceModel(), SIGNAL(layoutChanged()),
|
|
d, SLOT(onLayoutChanged()) );
|
|
}
|
|
|
|
QAbstractProxyModel::setSourceModel( model );
|
|
|
|
if ( sourceModel() ) {
|
|
connect( sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
|
|
d, SLOT(onDataChanged(QModelIndex,QModelIndex)) );
|
|
|
|
connect( sourceModel(), SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
|
|
d, SLOT(onHeaderDataChanged(Qt::Orientation,int,int)) );
|
|
|
|
connect( sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
|
|
d, SLOT(onRowsAboutToBeInserted(QModelIndex,int,int)) );
|
|
|
|
connect( sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
|
|
d, SLOT(onRowsInserted(QModelIndex,int,int)) );
|
|
|
|
connect( sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
|
|
d, SLOT(onRowsAboutToBeRemoved(QModelIndex,int,int)) );
|
|
|
|
connect( sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
|
|
d, SLOT(onRowsRemoved(QModelIndex,int,int)) );
|
|
|
|
connect( sourceModel(), SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)),
|
|
d, SLOT(onRowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)) );
|
|
|
|
connect( sourceModel(), SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
|
|
d, SLOT(onRowsMoved(QModelIndex,int,int,QModelIndex,int)) );
|
|
|
|
connect( sourceModel(), SIGNAL(modelAboutToBeReset()),
|
|
d, SLOT(onModelAboutToBeReset()) );
|
|
|
|
connect( sourceModel(), SIGNAL(modelReset()),
|
|
d, SLOT(onModelReset()) );
|
|
|
|
connect( sourceModel(), SIGNAL(layoutAboutToBeChanged()),
|
|
d, SLOT(onLayoutAboutToBeChanged()) );
|
|
|
|
connect( sourceModel(), SIGNAL(layoutChanged()),
|
|
d, SLOT(onLayoutChanged()) );
|
|
}
|
|
|
|
endResetModel();
|
|
}
|
|
|
|
void TodoModel::setIncidenceChanger( Akonadi::IncidenceChanger *changer )
|
|
{
|
|
d->m_changer = changer;
|
|
}
|
|
|
|
QVariant TodoModel::headerData( int column, Qt::Orientation orientation, int role ) const
|
|
{
|
|
if ( orientation != Qt::Horizontal ) {
|
|
return QVariant();
|
|
}
|
|
|
|
if ( role == Qt::DisplayRole ) {
|
|
switch ( column ) {
|
|
case SummaryColumn:
|
|
return QVariant( i18n( "Summary" ) );
|
|
case RecurColumn:
|
|
return QVariant( i18n( "Recurs" ) );
|
|
case PriorityColumn:
|
|
return QVariant( i18n( "Priority" ) );
|
|
case PercentColumn:
|
|
return QVariant( i18nc( "@title:column percent complete", "Complete" ) );
|
|
case StartDateColumn:
|
|
return QVariant( i18n( "Start Date" ) );
|
|
case DueDateColumn:
|
|
return QVariant( i18n( "Due Date" ) );
|
|
case CategoriesColumn:
|
|
return QVariant( i18n( "Categories" ) );
|
|
case DescriptionColumn:
|
|
return QVariant( i18n( "Description" ) );
|
|
case CalendarColumn:
|
|
return QVariant( i18n( "Calendar" ) );
|
|
}
|
|
}
|
|
|
|
if ( role == Qt::TextAlignmentRole ) {
|
|
switch ( column ) {
|
|
// If you change this, change data() too.
|
|
case RecurColumn:
|
|
case PriorityColumn:
|
|
case PercentColumn:
|
|
case StartDateColumn:
|
|
case DueDateColumn:
|
|
case CategoriesColumn:
|
|
case CalendarColumn:
|
|
return QVariant( Qt::AlignHCenter );
|
|
}
|
|
return QVariant();
|
|
}
|
|
return QVariant();
|
|
}
|
|
|
|
void TodoModel::setCalendar( const Akonadi::ETMCalendar::Ptr &calendar )
|
|
{
|
|
d->m_calendar = calendar;
|
|
}
|
|
|
|
Qt::DropActions TodoModel::supportedDropActions() const
|
|
{
|
|
// Qt::CopyAction not supported yet
|
|
return Qt::MoveAction;
|
|
}
|
|
|
|
QStringList TodoModel::mimeTypes() const
|
|
{
|
|
static QStringList list;
|
|
if ( list.isEmpty() ) {
|
|
list << KCalUtils::ICalDrag::mimeType() << KCalUtils::VCalDrag::mimeType();
|
|
}
|
|
return list;
|
|
}
|
|
|
|
QMimeData *TodoModel::mimeData( const QModelIndexList &indexes ) const
|
|
{
|
|
Akonadi::Item::List items;
|
|
Q_FOREACH ( const QModelIndex &index, indexes ) {
|
|
const Akonadi::Item item =
|
|
this->data( index, Akonadi::EntityTreeModel::ItemRole ).value<Akonadi::Item>();
|
|
if ( item.isValid() && !items.contains( item ) ) {
|
|
items.push_back( item );
|
|
}
|
|
}
|
|
return CalendarSupport::createMimeData( items, d->m_calendar->timeSpec() );
|
|
}
|
|
|
|
bool TodoModel::dropMimeData( const QMimeData *data, Qt::DropAction action,
|
|
int row, int column, const QModelIndex &parent )
|
|
{
|
|
Q_UNUSED( row );
|
|
Q_UNUSED( column );
|
|
|
|
if ( action != Qt::MoveAction ) {
|
|
kWarning() << "No action other than MoveAction currently supported!"; //TODO
|
|
return false;
|
|
}
|
|
|
|
if ( d->m_calendar && d->m_changer &&
|
|
( KCalUtils::ICalDrag::canDecode( data ) || KCalUtils::VCalDrag::canDecode( data ) ) ) {
|
|
KCalUtils::DndFactory dndFactory( d->m_calendar );
|
|
KCalCore::Todo::Ptr t = dndFactory.createDropTodo( data );
|
|
KCalCore::Event::Ptr e = dndFactory.createDropEvent( data );
|
|
|
|
if ( t ) {
|
|
// we don't want to change the created todo, but the one which is already
|
|
// stored in our calendar / tree
|
|
const Akonadi::Item item = d->findItemByUid( t->uid(), QModelIndex() );
|
|
KCalCore::Todo::Ptr todo = CalendarSupport::todo( item );
|
|
KCalCore::Todo::Ptr destTodo;
|
|
if ( parent.isValid() ) {
|
|
const Akonadi::Item parentItem =
|
|
this->data( parent, Akonadi::EntityTreeModel::ItemRole ).value<Akonadi::Item>();
|
|
if ( parentItem.isValid() ) {
|
|
destTodo = CalendarSupport::todo( parentItem );
|
|
}
|
|
}
|
|
|
|
KCalCore::Incidence::Ptr tmp = destTodo;
|
|
while ( tmp ) {
|
|
if ( tmp->uid() == todo->uid() ) { //correct, don't use instanceIdentifier() here
|
|
KMessageBox::information(
|
|
0,
|
|
i18n( "Cannot move to-do to itself or a child of itself." ),
|
|
i18n( "Drop To-do" ), QLatin1String("NoDropTodoOntoItself") );
|
|
return false;
|
|
}
|
|
const QString parentUid = tmp->relatedTo();
|
|
tmp = CalendarSupport::incidence( d->m_calendar->item( parentUid ) );
|
|
}
|
|
|
|
if (!destTodo || !destTodo->hasRecurrenceId()) {
|
|
KCalCore::Todo::Ptr oldTodo = KCalCore::Todo::Ptr( todo->clone() );
|
|
// destTodo is empty when we drag a to-do out of a relationship
|
|
todo->setRelatedTo( destTodo ? destTodo->uid() : QString() );
|
|
d->m_changer->modifyIncidence( item, oldTodo );
|
|
|
|
// again, no need to emit dataChanged, that's done by processChange
|
|
return true;
|
|
} else {
|
|
kDebug() << "Todo's with recurring id can't have child todos yet.";
|
|
return false;
|
|
}
|
|
|
|
} else if ( e ) {
|
|
// TODO: Implement dropping an event onto a to-do: Generate a relationship to the event!
|
|
} else {
|
|
if ( !parent.isValid() ) {
|
|
// TODO we should create a new todo with the data in the drop object
|
|
kDebug() << "TODO: Create a new todo with the given data";
|
|
return false;
|
|
}
|
|
|
|
const Akonadi::Item parentItem =
|
|
this->data( parent, Akonadi::EntityTreeModel::ItemRole ).value<Akonadi::Item>();
|
|
KCalCore::Todo::Ptr destTodo = CalendarSupport::todo( parentItem );
|
|
|
|
if ( data->hasText() ) {
|
|
QString text = data->text();
|
|
|
|
KCalCore::Todo::Ptr oldTodo = KCalCore::Todo::Ptr( destTodo->clone() );
|
|
|
|
if ( text.startsWith( QLatin1String( "file:" ) ) ) {
|
|
destTodo->addAttachment( KCalCore::Attachment::Ptr( new KCalCore::Attachment( text ) ) );
|
|
} else {
|
|
QStringList emails = KPIMUtils::splitAddressList( text );
|
|
for ( QStringList::ConstIterator it = emails.constBegin();
|
|
it != emails.constEnd(); ++it ) {
|
|
QString name, email, comment;
|
|
if ( KPIMUtils::splitAddress( *it, name, email, comment ) ==
|
|
KPIMUtils::AddressOk ) {
|
|
destTodo->addAttendee(
|
|
KCalCore::Attendee::Ptr( new KCalCore::Attendee( name, email ) ) );
|
|
}
|
|
}
|
|
}
|
|
d->m_changer->modifyIncidence( parentItem, oldTodo );
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Qt::ItemFlags TodoModel::flags( const QModelIndex &index ) const
|
|
{
|
|
if ( !index.isValid() ) {
|
|
return 0;
|
|
}
|
|
|
|
Qt::ItemFlags ret = QAbstractItemModel::flags( index );
|
|
|
|
const Akonadi::Item item =
|
|
data( index, Akonadi::EntityTreeModel::ItemRole ).value<Akonadi::Item>();
|
|
|
|
if ( !item.isValid() ) {
|
|
Q_ASSERT( mapToSource( index ).isValid() );
|
|
kWarning() << "Item is invalid " << index;
|
|
Q_ASSERT( false );
|
|
return 0;
|
|
}
|
|
|
|
ret |= Qt::ItemIsDragEnabled;
|
|
|
|
const KCalCore::Todo::Ptr todo = CalendarSupport::todo( item );
|
|
|
|
if ( d->m_calendar->hasRight( item, Akonadi::Collection::CanChangeItem ) ) {
|
|
// the following columns are editable:
|
|
switch ( index.column() ) {
|
|
case SummaryColumn:
|
|
case PriorityColumn:
|
|
case PercentColumn:
|
|
case StartDateColumn:
|
|
case DueDateColumn:
|
|
case CategoriesColumn:
|
|
ret |= Qt::ItemIsEditable;
|
|
break;
|
|
case DescriptionColumn:
|
|
if ( !todo->descriptionIsRich() ) {
|
|
ret |= Qt::ItemIsEditable;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( index.column() == 0 ) {
|
|
// whole rows should have checkboxes, so append the flag for the
|
|
// first item of every row only. Also, only the first item of every
|
|
// row should be used as a target for a drag and drop operation.
|
|
ret |= Qt::ItemIsUserCheckable |
|
|
Qt::ItemIsDropEnabled;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
QModelIndex TodoModel::mapFromSource( const QModelIndex &sourceIndex ) const
|
|
{
|
|
if ( !sourceModel() || !sourceIndex.isValid() ) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
Q_ASSERT( sourceIndex.internalPointer() );
|
|
|
|
return createIndex( sourceIndex.row(), 0, sourceIndex.internalPointer() );
|
|
}
|
|
|
|
QModelIndex TodoModel::mapToSource( const QModelIndex &proxyIndex ) const
|
|
{
|
|
if ( !sourceModel() || !proxyIndex.isValid() ) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
if ( proxyIndex.column() != 0 ) {
|
|
kError() << "Map to source called with column>0, but source model only has 1 column";
|
|
Q_ASSERT( false );
|
|
}
|
|
|
|
Q_ASSERT( proxyIndex.internalPointer() );
|
|
|
|
// we convert to column 0
|
|
const QModelIndex sourceIndex = SourceModelIndex( proxyIndex.row(), 0,
|
|
proxyIndex.internalPointer(), sourceModel() );
|
|
|
|
return sourceIndex;
|
|
}
|
|
|
|
QModelIndex TodoModel::index( int row, int column, const QModelIndex &parent ) const
|
|
{
|
|
if ( !sourceModel() ) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
Q_ASSERT( !parent.isValid() || parent.internalPointer() );
|
|
QModelIndex parent_col0 =
|
|
parent.isValid() ?
|
|
createIndex( parent.row(), 0, parent.internalPointer() ) :
|
|
QModelIndex();
|
|
|
|
// Lets preserve the original internalPointer
|
|
const QModelIndex index =
|
|
mapFromSource( sourceModel()->index( row, 0, mapToSource( parent_col0 ) ) );
|
|
|
|
Q_ASSERT( !index.isValid() || index.internalPointer() );
|
|
|
|
if ( index.isValid() ) {
|
|
return createIndex( row, column, index.internalPointer() );
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex TodoModel::parent( const QModelIndex &child ) const
|
|
{
|
|
if ( !sourceModel() || !child.isValid() ) {
|
|
return QModelIndex();
|
|
}
|
|
|
|
Q_ASSERT( child.internalPointer() );
|
|
const QModelIndex child_col0 = createIndex( child.row(), 0, child.internalPointer() );
|
|
QModelIndex parentIndex = mapFromSource( sourceModel()->parent( mapToSource( child_col0 ) ) );
|
|
|
|
Q_ASSERT( !parentIndex.isValid() || parentIndex.internalPointer() );
|
|
if ( parentIndex.isValid() ) { // preserve original column
|
|
return createIndex( parentIndex.row(), child.column(), parentIndex.internalPointer() );
|
|
}
|
|
|
|
return QModelIndex();
|
|
}
|
|
|
|
QModelIndex TodoModel::buddy( const QModelIndex &index ) const
|
|
{
|
|
// We reimplement because the default implementation calls mapToSource() and
|
|
// source model doesn't have the same number of columns.
|
|
return index;
|
|
}
|