File indexing completed on 2024-05-12 05:10:50

0001 /*
0002   SPDX-FileCopyrightText: 2008 Thomas Thrainer <tom_t@gmx.at>
0003   SPDX-FileCopyrightText: 2012 Sérgio Martins <iamsergio@gmail.com>
0004 
0005   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0006 */
0007 
0008 #include "todomodel.h"
0009 
0010 #include <KCalendarCore/Attachment>
0011 #include <KCalendarCore/Event>
0012 #include <KEmailAddress>
0013 #include <KLocalizedString>
0014 
0015 #include <Akonadi/CalendarUtils>
0016 #include <Akonadi/IncidenceTreeModel>
0017 #include <Akonadi/TagCache>
0018 
0019 #include <KCalUtils/DndFactory>
0020 #include <KCalUtils/ICalDrag>
0021 #include <KCalUtils/IncidenceFormatter>
0022 #include <KCalUtils/VCalDrag>
0023 
0024 #include "akonadicalendar_debug.h"
0025 
0026 #include <QIcon>
0027 #include <QMimeData>
0028 
0029 
0030 namespace Akonadi
0031 {
0032 class TodoModelPrivate
0033 {
0034 public:
0035     TodoModelPrivate(TodoModel *qq);
0036 
0037     Akonadi::EntityTreeModel *etm()
0038     {
0039         if (!mEtm) {
0040             auto *model = q->sourceModel();
0041             while (model) {
0042                 if (auto *proxy = qobject_cast<QAbstractProxyModel *>(model); proxy) {
0043                     model = proxy->sourceModel();
0044                 } else if (auto *etm = qobject_cast<Akonadi::EntityTreeModel *>(model); etm) {
0045                     mEtm = etm;
0046                     break;
0047                 } else {
0048                     return nullptr;
0049                 }
0050             }
0051         }
0052 
0053         return mEtm;
0054     }
0055 
0056     // TODO: O(N) complexity, see if the profiler complains about this
0057     Akonadi::Item findItemByUid(const QString &uid, const QModelIndex &parent) const;
0058 
0059     Akonadi::IncidenceChanger *m_changer = nullptr;
0060 
0061     void onDataChanged(const QModelIndex &begin, const QModelIndex &end);
0062 
0063     TodoModel *const q;
0064     Akonadi::EntityTreeModel *mEtm = nullptr;
0065 };
0066 }
0067 
0068 using namespace Akonadi;
0069 
0070 TodoModelPrivate::TodoModelPrivate(TodoModel *qq)
0071     : q(qq)
0072 {
0073 }
0074 
0075 Akonadi::Item TodoModelPrivate::findItemByUid(const QString &uid, const QModelIndex &parent) const
0076 {
0077     Q_ASSERT(!uid.isEmpty());
0078     auto treeModel = qobject_cast<Akonadi::IncidenceTreeModel *>(q->sourceModel());
0079     if (treeModel) { // O(1) Shortcut
0080         return treeModel->item(uid);
0081     }
0082 
0083     Akonadi::Item item;
0084     const int count = q->rowCount(parent);
0085     for (int i = 0; i < count; ++i) {
0086         const QModelIndex currentIndex = q->index(i, 0, parent);
0087         Q_ASSERT(currentIndex.isValid());
0088         item = q->data(currentIndex, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0089         if (item.isValid()) {
0090             return item;
0091         } else {
0092             item = findItemByUid(uid, currentIndex);
0093             if (item.isValid()) {
0094                 return item;
0095             }
0096         }
0097     }
0098 
0099     return item;
0100 }
0101 
0102 void TodoModelPrivate::onDataChanged(const QModelIndex &begin, const QModelIndex &end)
0103 {
0104     Q_ASSERT(begin.isValid());
0105     Q_ASSERT(end.isValid());
0106     const QModelIndex proxyBegin = q->mapFromSource(begin);
0107     Q_ASSERT(proxyBegin.column() == 0);
0108     const QModelIndex proxyEnd = q->mapFromSource(end);
0109     Q_EMIT q->dataChanged(proxyBegin, proxyEnd.sibling(proxyEnd.row(), TodoModel::ColumnCount - 1));
0110 }
0111 
0112 TodoModel::TodoModel(QObject *parent)
0113     : KExtraColumnsProxyModel(parent)
0114     , d(new TodoModelPrivate(this))
0115 {
0116     setObjectName(QLatin1StringView("TodoModel"));
0117 }
0118 
0119 TodoModel::~TodoModel() = default;
0120 
0121 QVariant TodoModel::data(const QModelIndex &index, int role) const
0122 {
0123     Q_ASSERT(index.isValid());
0124     if (!index.isValid()) {
0125         return {};
0126     }
0127 
0128     const QModelIndex sourceIndex = mapToSource(index.sibling(index.row(), 0));
0129     if (!sourceIndex.isValid()) {
0130         return {};
0131     }
0132     Q_ASSERT(sourceIndex.isValid());
0133     const auto item = sourceIndex.data(Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0134     if (!item.isValid()) {
0135         qCWarning(AKONADICALENDAR_LOG) << "Invalid index: " << sourceIndex;
0136         // Q_ASSERT( false );
0137         return {};
0138     }
0139     const KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(item);
0140     if (!todo) {
0141         qCCritical(AKONADICALENDAR_LOG) << "item.hasPayload()" << item.hasPayload();
0142         if (item.hasPayload<KCalendarCore::Incidence::Ptr>()) {
0143             auto incidence = item.payload<KCalendarCore::Incidence::Ptr>();
0144             if (incidence) {
0145                 qCCritical(AKONADICALENDAR_LOG) << "It's actually " << incidence->type();
0146             }
0147         }
0148 
0149         Q_ASSERT(!"There's no to-do.");
0150         return {};
0151     }
0152 
0153     switch (role) {
0154     case SummaryRole:
0155         return todo->summary();
0156     case RecurRole:
0157         if (todo->recurs()) {
0158             if (todo->hasRecurrenceId()) {
0159                 return i18nc("yes, an exception to a recurring to-do", "Exception");
0160             } else {
0161                 return i18nc("yes, recurring to-do", "Yes");
0162             }
0163         } else {
0164             return i18nc("no, not a recurring to-do", "No");
0165         }
0166     case PriorityRole:
0167         if (todo->priority() == 0) {
0168             return QStringLiteral("--");
0169         }
0170         return todo->priority();
0171     case PercentRole:
0172         return todo->percentComplete();
0173     case StartDateRole:
0174         return todo->hasStartDate() ? QLocale().toString(todo->dtStart().toLocalTime().date(), QLocale::ShortFormat) : QString();
0175     case DueDateRole:
0176         return todo->hasDueDate() ? QLocale().toString(todo->dtDue().toLocalTime().date(), QLocale::ShortFormat) : QString();
0177     case CategoriesRole:
0178         return todo->categories().join(i18nc("delimiter for joining category/tag names", ","));
0179     case DescriptionRole:
0180         return todo->description();
0181     case CalendarRole:
0182         return Akonadi::CalendarUtils::displayName(d->etm(), item.parentCollection());
0183     default:
0184         break; // column based model handling
0185     }
0186 
0187     if (role == Qt::DisplayRole) {
0188         switch (index.column()) {
0189         case SummaryColumn:
0190             return QVariant(todo->summary());
0191         case RecurColumn:
0192             if (todo->recurs()) {
0193                 if (todo->hasRecurrenceId()) {
0194                     return i18nc("yes, an exception to a recurring to-do", "Exception");
0195                 } else {
0196                     return i18nc("yes, recurring to-do", "Yes");
0197                 }
0198             } else {
0199                 return i18nc("no, not a recurring to-do", "No");
0200             }
0201         case PriorityColumn:
0202             if (todo->priority() == 0) {
0203                 return QVariant(QStringLiteral("--"));
0204             }
0205             return {todo->priority()};
0206         case PercentColumn:
0207             return {todo->percentComplete()};
0208         case StartDateColumn:
0209             return todo->hasStartDate() ? QLocale().toString(todo->dtStart().toLocalTime().date(), QLocale::ShortFormat) : QVariant(QString());
0210         case DueDateColumn:
0211             return todo->hasDueDate() ? QLocale().toString(todo->dtDue().toLocalTime().date(), QLocale::ShortFormat) : QVariant(QString());
0212         case CompletedDateColumn:
0213             return todo->hasCompletedDate() ? QLocale().toString(todo->completed().toLocalTime().date(), QLocale::ShortFormat) : QVariant(QString());
0214         case CategoriesColumn: {
0215             QString categories = todo->categories().join(i18nc("delimiter for joining category/tag names", ","));
0216             return QVariant(categories);
0217         }
0218         case DescriptionColumn:
0219             return QVariant(todo->description());
0220         case CalendarColumn:
0221             return QVariant(Akonadi::CalendarUtils::displayName(d->etm(), item.parentCollection()));
0222         }
0223         return {};
0224     }
0225 
0226     if (role == Qt::EditRole) {
0227         switch (index.column()) {
0228         case SummaryColumn:
0229             return QVariant(todo->summary());
0230         case RecurColumn:
0231             return {todo->recurs()};
0232         case PriorityColumn:
0233             return {todo->priority()};
0234         case PercentColumn:
0235             return {todo->percentComplete()};
0236         case StartDateColumn:
0237             return QVariant(todo->dtStart().date());
0238         case DueDateColumn:
0239             return QVariant(todo->dtDue().date());
0240         case CompletedDateColumn:
0241             return QVariant(todo->completed().date());
0242         case CategoriesColumn:
0243             return QVariant(todo->categories());
0244         case DescriptionColumn:
0245             return QVariant(todo->description());
0246         case CalendarColumn:
0247             return QVariant(Akonadi::CalendarUtils::displayName(d->etm(), item.parentCollection()));
0248         }
0249         return {};
0250     }
0251 
0252     // indicate if a row is checked (=completed) only in the first column
0253     if (role == Qt::CheckStateRole && index.column() == 0) {
0254         if (hasChildren(index) && !index.parent().isValid()) {
0255             return {};
0256         }
0257 
0258         if (todo->isCompleted()) {
0259             return {Qt::Checked};
0260         } else {
0261             return {Qt::Unchecked};
0262         }
0263     }
0264 
0265     // icon for recurring todos
0266     // It's in the summary column so you don't accidentally click
0267     // the checkbox ( which increments the next occurrence date ).
0268     // category colour
0269     if (role == Qt::DecorationRole && index.column() == SummaryColumn) {
0270         if (todo->recurs()) {
0271             return QVariant(QIcon::fromTheme(QStringLiteral("task-recurring")));
0272         }
0273         const QStringList categories = todo->categories();
0274         return categories.isEmpty() ? QVariant() : QVariant(Akonadi::TagCache::instance()->tagColor(categories.first()));
0275     } else if (role == Qt::DecorationRole) {
0276         return {};
0277     }
0278 
0279     if (role == TodoRole) {
0280         return QVariant::fromValue(item);
0281     }
0282 
0283     if (role == TodoPtrRole) {
0284         return QVariant::fromValue(todo);
0285     }
0286 
0287     if (role == IsRichTextRole) {
0288         if (index.column() == SummaryColumn) {
0289             return {todo->summaryIsRich()};
0290         } else if (index.column() == DescriptionColumn) {
0291             return {todo->descriptionIsRich()};
0292         } else {
0293             return {};
0294         }
0295     }
0296 
0297     if (role == Qt::TextAlignmentRole) {
0298         switch (index.column()) {
0299         // If you change this, change headerData() too.
0300         case RecurColumn:
0301         case PriorityColumn:
0302         case PercentColumn:
0303         case StartDateColumn:
0304         case DueDateColumn:
0305         case CategoriesColumn:
0306         case CalendarColumn:
0307             return {Qt::AlignHCenter | Qt::AlignVCenter};
0308         }
0309         return {Qt::AlignLeft | Qt::AlignVCenter};
0310     }
0311 
0312     if (sourceModel()) {
0313         return sourceModel()->data(mapToSource(index.sibling(index.row(), 0)), role);
0314     }
0315 
0316     return {};
0317 }
0318 
0319 QVariant TodoModel::extraColumnData(const QModelIndex &parent, int row, int extraColumn, int role) const
0320 {
0321     // we customize all columns, not just the extra ones, and thus do all that in ::data()
0322     Q_UNUSED(parent);
0323     Q_UNUSED(row);
0324     Q_UNUSED(extraColumn);
0325     Q_UNUSED(role);
0326     return {};
0327 }
0328 
0329 bool TodoModel::setData(const QModelIndex &index, const QVariant &value, int role)
0330 {
0331     Q_ASSERT(index.isValid());
0332     if (!d->m_changer) {
0333         return false;
0334     }
0335     const QVariant oldValue = data(index, role);
0336 
0337     if (oldValue == value) {
0338         // Nothing changed, the user used one of the QStyledDelegate's editors but seted the old value
0339         // Lets just skip this then and avoid a roundtrip to akonadi, and avoid sending invitations
0340         return true;
0341     }
0342 
0343     const auto item = data(index, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0344     const KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(item);
0345 
0346     if (!item.isValid() || !todo) {
0347         qCWarning(AKONADICALENDAR_LOG) << "TodoModel::setData() called, bug item is invalid or doesn't have payload";
0348         Q_ASSERT(false);
0349         return false;
0350     }
0351 
0352     const auto parentCol = Akonadi::EntityTreeModel::updatedCollection(d->etm(), item.parentCollection());
0353     if (parentCol.rights() & Akonadi::Collection::CanChangeItem) {
0354         KCalendarCore::Todo::Ptr oldTodo(todo->clone());
0355         if (role == Qt::CheckStateRole && index.column() == 0) {
0356             const bool checked = static_cast<Qt::CheckState>(value.toInt()) == Qt::Checked;
0357             if (checked) {
0358                 todo->setCompleted(QDateTime::currentDateTimeUtc()); // Because it calls Todo::recurTodo()
0359             } else {
0360                 todo->setCompleted(false);
0361             }
0362         }
0363 
0364         if (role == Qt::EditRole) {
0365             switch (index.column()) {
0366             case SummaryColumn:
0367                 if (!value.toString().isEmpty()) {
0368                     todo->setSummary(value.toString());
0369                 }
0370                 break;
0371             case PriorityColumn:
0372                 todo->setPriority(value.toInt());
0373                 break;
0374             case PercentColumn:
0375                 todo->setPercentComplete(value.toInt());
0376                 break;
0377             case StartDateColumn: {
0378                 QDateTime tmp = todo->dtStart();
0379                 tmp.setDate(value.toDate());
0380                 todo->setDtStart(tmp);
0381                 break;
0382             }
0383             case DueDateColumn: {
0384                 QDateTime tmp = todo->dtDue();
0385                 tmp.setDate(value.toDate());
0386                 todo->setDtDue(tmp);
0387                 break;
0388             }
0389             case CategoriesColumn:
0390                 todo->setCategories(value.toStringList());
0391                 break;
0392             case DescriptionColumn:
0393                 todo->setDescription(value.toString());
0394                 break;
0395             }
0396         }
0397 
0398         if (!todo->dirtyFields().isEmpty()) {
0399             d->m_changer->modifyIncidence(item, oldTodo);
0400             // modifyIncidence will eventually call the view's
0401             // changeIncidenceDisplay method, which in turn
0402             // will call processChange. processChange will then emit
0403             // dataChanged to the view, so we don't have to
0404             // do it here
0405         }
0406 
0407         return true;
0408     } else {
0409         if (!(role == Qt::CheckStateRole && index.column() == 0)) {
0410             // KOHelper::showSaveIncidenceErrorMsg( 0, todo ); //TODO pass parent
0411             qCCritical(AKONADICALENDAR_LOG) << "Unable to modify incidence";
0412         }
0413         return false;
0414     }
0415 }
0416 
0417 int TodoModel::columnCount(const QModelIndex &) const
0418 {
0419     return ColumnCount;
0420 }
0421 
0422 void TodoModel::setSourceModel(QAbstractItemModel *model)
0423 {
0424     if (model == sourceModel()) {
0425         return;
0426     }
0427 
0428     beginResetModel();
0429 
0430     if (sourceModel()) {
0431         disconnect(sourceModel(), &QAbstractItemModel::dataChanged, this, nullptr);
0432     }
0433 
0434     KExtraColumnsProxyModel::setSourceModel(model);
0435 
0436     if (sourceModel()) {
0437         connect(sourceModel(), &QAbstractItemModel::dataChanged, this, [this](const auto &begin, const auto &end) {
0438             d->onDataChanged(begin, end);
0439         });
0440     }
0441 
0442     endResetModel();
0443 }
0444 
0445 void TodoModel::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
0446 {
0447     d->m_changer = changer;
0448 }
0449 
0450 QVariant TodoModel::headerData(int column, Qt::Orientation orientation, int role) const
0451 {
0452     if (orientation != Qt::Horizontal) {
0453         return {};
0454     }
0455 
0456     if (role == Qt::DisplayRole) {
0457         switch (column) {
0458         case SummaryColumn:
0459             return QVariant(i18n("Summary"));
0460         case RecurColumn:
0461             return QVariant(i18n("Recurs"));
0462         case PriorityColumn:
0463             return QVariant(i18n("Priority"));
0464         case PercentColumn:
0465             return QVariant(i18nc("@title:column percent complete", "Complete"));
0466         case StartDateColumn:
0467             return QVariant(i18n("Start Date"));
0468         case DueDateColumn:
0469             return QVariant(i18n("Due Date"));
0470         case CompletedDateColumn:
0471             return QVariant(i18nc("@title:column date completed", "Completed"));
0472         case CategoriesColumn:
0473             return QVariant(i18n("Tags"));
0474         case DescriptionColumn:
0475             return QVariant(i18n("Description"));
0476         case CalendarColumn:
0477             return QVariant(i18n("Calendar"));
0478         }
0479     }
0480 
0481     if (role == Qt::TextAlignmentRole) {
0482         switch (column) {
0483         // If you change this, change data() too.
0484         case RecurColumn:
0485         case PriorityColumn:
0486         case PercentColumn:
0487         case StartDateColumn:
0488         case DueDateColumn:
0489         case CategoriesColumn:
0490         case CalendarColumn:
0491             return {Qt::AlignHCenter};
0492         }
0493         return {};
0494     }
0495     return {};
0496 }
0497 
0498 void TodoModel::setCalendar(const Akonadi::ETMCalendar::Ptr &calendar)
0499 {
0500     Q_UNUSED(calendar);
0501     // Deprecated, no longer does anything
0502 }
0503 
0504 Qt::DropActions TodoModel::supportedDropActions() const
0505 {
0506     // Qt::CopyAction not supported yet
0507     return Qt::MoveAction;
0508 }
0509 
0510 QStringList TodoModel::mimeTypes() const
0511 {
0512     static QStringList list;
0513     if (list.isEmpty()) {
0514         list << KCalUtils::ICalDrag::mimeType() << KCalUtils::VCalDrag::mimeType();
0515     }
0516     return list;
0517 }
0518 
0519 QMimeData *TodoModel::mimeData(const QModelIndexList &indexes) const
0520 {
0521     Akonadi::Item::List items;
0522     for (const QModelIndex &index : indexes) {
0523         const auto item = this->data(index, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0524         if (item.isValid() && !items.contains(item)) {
0525             items.push_back(item);
0526         }
0527     }
0528     return Akonadi::CalendarUtils::createMimeData(items);
0529 }
0530 
0531 bool TodoModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
0532 {
0533     Q_UNUSED(row)
0534     Q_UNUSED(column)
0535 
0536     if (action != Qt::MoveAction) {
0537         qCWarning(AKONADICALENDAR_LOG) << "No action other than MoveAction currently supported!"; // TODO
0538         return false;
0539     }
0540 
0541     if (d->m_changer && (KCalUtils::ICalDrag::canDecode(data) || KCalUtils::VCalDrag::canDecode(data))) {
0542         // DndFactory only needs a valid calendar for drag event, not for drop event.
0543         KCalUtils::DndFactory dndFactory(KCalendarCore::Calendar::Ptr{});
0544         KCalendarCore::Todo::Ptr t = dndFactory.createDropTodo(data);
0545         KCalendarCore::Event::Ptr e = dndFactory.createDropEvent(data);
0546 
0547         if (t) {
0548             // we don't want to change the created todo, but the one which is already
0549             // stored in our calendar / tree
0550             const Akonadi::Item item = d->findItemByUid(t->uid(), QModelIndex());
0551             KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(item);
0552             KCalendarCore::Todo::Ptr destTodo;
0553             if (parent.isValid()) {
0554                 const auto parentItem = this->data(parent, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0555                 if (parentItem.isValid()) {
0556                     destTodo = Akonadi::CalendarUtils::todo(parentItem);
0557                 }
0558 
0559                 auto tmpParent = parent;
0560                 while (tmpParent.isValid()) {
0561                     const auto parentItem = this->data(parent, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0562                     if (!parentItem.isValid()) {
0563                         break;
0564                     }
0565                     const auto parentTodo = Akonadi::CalendarUtils::todo(parentItem);
0566                     if (!parentTodo) {
0567                         break;
0568                     }
0569 
0570                     if (parentTodo->uid() == todo->uid()) { // correct, don't use instanceIdentifier() here
0571                         Q_EMIT dropOnSelfRejected();
0572                         return false;
0573                     }
0574 
0575                     tmpParent = tmpParent.parent();
0576                 }
0577             }
0578 
0579             if (!destTodo || !destTodo->hasRecurrenceId()) {
0580                 KCalendarCore::Todo::Ptr oldTodo = KCalendarCore::Todo::Ptr(todo->clone());
0581                 // destTodo is empty when we drag a to-do out of a relationship
0582                 todo->setRelatedTo(destTodo ? destTodo->uid() : QString());
0583                 d->m_changer->modifyIncidence(item, oldTodo);
0584 
0585                 // again, no need to Q_EMIT dataChanged, that's done by processChange
0586                 return true;
0587             } else {
0588                 qCDebug(AKONADICALENDAR_LOG) << "Todo's with recurring id can't have child todos yet.";
0589                 return false;
0590             }
0591         } else if (e) {
0592             // TODO: Implement dropping an event onto a to-do: Generate a relationship to the event!
0593         } else {
0594             if (!parent.isValid()) {
0595                 // TODO we should create a new todo with the data in the drop object
0596                 qCDebug(AKONADICALENDAR_LOG) << "TODO: Create a new todo with the given data";
0597                 return false;
0598             }
0599 
0600             const auto parentItem = this->data(parent, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0601             KCalendarCore::Todo::Ptr destTodo = Akonadi::CalendarUtils::todo(parentItem);
0602 
0603             if (data->hasText()) {
0604                 QString text = data->text();
0605 
0606                 KCalendarCore::Todo::Ptr oldTodo = KCalendarCore::Todo::Ptr(destTodo->clone());
0607 
0608                 if (text.startsWith(QLatin1StringView("file:"))) {
0609                     destTodo->addAttachment(KCalendarCore::Attachment(text));
0610                 } else {
0611                     QStringList emails = KEmailAddress::splitAddressList(text);
0612                     for (QStringList::ConstIterator it = emails.constBegin(); it != emails.constEnd(); ++it) {
0613                         QString name;
0614                         QString email;
0615                         QString comment;
0616                         if (KEmailAddress::splitAddress(*it, name, email, comment) == KEmailAddress::AddressOk) {
0617                             destTodo->addAttendee(KCalendarCore::Attendee(name, email));
0618                         }
0619                     }
0620                 }
0621                 d->m_changer->modifyIncidence(parentItem, oldTodo);
0622                 return true;
0623             }
0624         }
0625     }
0626 
0627     return false;
0628 }
0629 
0630 Qt::ItemFlags TodoModel::flags(const QModelIndex &index) const
0631 {
0632     if (!index.isValid()) {
0633         return Qt::NoItemFlags;
0634     }
0635 
0636     Qt::ItemFlags ret = KExtraColumnsProxyModel::flags(index);
0637 
0638     const auto item = data(index, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0639 
0640     if (!item.isValid()) {
0641         Q_ASSERT(mapToSource(index).isValid());
0642         qCWarning(AKONADICALENDAR_LOG) << "Item is invalid " << index;
0643         Q_ASSERT(false);
0644         return Qt::NoItemFlags;
0645     }
0646 
0647     ret |= Qt::ItemIsDragEnabled;
0648 
0649     const KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(item);
0650 
0651     const auto parentCol = Akonadi::EntityTreeModel::updatedCollection(d->etm(), item.parentCollection());
0652     if (parentCol.rights() & Akonadi::Collection::CanChangeItem) {
0653         // the following columns are editable:
0654         switch (index.column()) {
0655         case SummaryColumn:
0656         case PriorityColumn:
0657         case PercentColumn:
0658         case StartDateColumn:
0659         case DueDateColumn:
0660         case CategoriesColumn:
0661             ret |= Qt::ItemIsEditable;
0662             break;
0663         case DescriptionColumn:
0664             if (!todo->descriptionIsRich()) {
0665                 ret |= Qt::ItemIsEditable;
0666             }
0667             break;
0668         }
0669     }
0670 
0671     if (index.column() == 0) {
0672         // whole rows should have checkboxes, so append the flag for the
0673         // first item of every row only. Also, only the first item of every
0674         // row should be used as a target for a drag and drop operation.
0675         ret |= Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled;
0676     }
0677     return ret;
0678 }
0679 
0680 QHash<int, QByteArray> TodoModel::roleNames() const
0681 {
0682     return {
0683         {SummaryRole, QByteArrayLiteral("summary")},
0684         {RecurRole, QByteArrayLiteral("recur")},
0685         {PriorityRole, QByteArrayLiteral("priority")},
0686         {PercentRole, QByteArrayLiteral("percent")},
0687         {StartDateRole, QByteArrayLiteral("startDate")},
0688         {DueDateRole, QByteArrayLiteral("dueDate")},
0689         {CategoriesRole, QByteArrayLiteral("categories")},
0690         {DescriptionRole, QByteArrayLiteral("description")},
0691         {CalendarRole, QByteArrayLiteral("calendar")},
0692         {Qt::CheckStateRole, QByteArrayLiteral("checked")},
0693     };
0694 }
0695 
0696 #include "moc_todomodel.cpp"