File indexing completed on 2024-05-19 05:19:21

0001 /*
0002     This file is part of KJots.
0003 
0004     SPDX-FileCopyrightText: 2009 Stephen Kelly <steveire@gmail.com>
0005                   2020 Igor Poboiko <igor.poboiko@gmail.com>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "kjotsmodel.h"
0011 
0012 #include <QAbstractProxyModel>
0013 #include <QTextDocument>
0014 #include <QIcon>
0015 
0016 #include <akonadi_version.h>
0017 #include <Akonadi/ChangeRecorder>
0018 #include <Akonadi/EntityDisplayAttribute>
0019 #include <Akonadi/NoteUtils>
0020 
0021 #include <KMime/Message>
0022 #include <KPIMTextEdit/TextUtils>
0023 #include <KPIMTextEdit/RichTextComposerImages>
0024 #include <KPIMTextEdit/TextHTMLBuilder>
0025 #include <KPIMTextEdit/MarkupDirector>
0026 #include <KPIMTextEdit/PlainTextMarkupBuilder>
0027 #include <KLocalizedString>
0028 #include <KFormat>
0029 
0030 #include "noteshared/notelockattribute.h"
0031 #include "noteshared/notepinattribute.h"
0032 
0033 Q_DECLARE_METATYPE(QTextDocument *)
0034 Q_DECLARE_METATYPE(KPIMTextEdit::ImageList)
0035 
0036 using namespace Akonadi;
0037 
0038 KJotsEntity::KJotsEntity(const QModelIndex &index, QObject *parent)
0039     : QObject(parent)
0040     , m_index(index)
0041 {
0042 }
0043 
0044 void KJotsEntity::setIndex(const QModelIndex &index)
0045 {
0046     m_index = QPersistentModelIndex(index);
0047 }
0048 
0049 QString KJotsEntity::title() const
0050 {
0051     return m_index.data().toString();
0052 }
0053 
0054 QString KJotsEntity::content() const
0055 {
0056     auto *document = m_index.data(KJotsModel::DocumentRole).value<QTextDocument *>();
0057     if (!document) {
0058         return QString();
0059     }
0060 
0061     KPIMTextEdit::TextHTMLBuilder builder;
0062     KPIMTextEdit::MarkupDirector director(&builder);
0063 
0064     director.processDocument(document);
0065     QString result = builder.getResult();
0066 
0067     return result;
0068 }
0069 
0070 QString KJotsEntity::plainContent() const
0071 {
0072     auto *document = m_index.data(KJotsModel::DocumentRole).value<QTextDocument *>();
0073     if (!document) {
0074         return QString();
0075     }
0076 
0077     KPIMTextEdit::PlainTextMarkupBuilder builder;
0078     KPIMTextEdit::MarkupDirector director(&builder);
0079 
0080     director.processDocument(document);
0081     const QString result = builder.getResult();
0082 
0083     return result;
0084 }
0085 
0086 QString KJotsEntity::url() const
0087 {
0088     return m_index.data(KJotsModel::EntityUrlRole).toString();
0089 }
0090 
0091 qint64 KJotsEntity::entityId() const
0092 {
0093     Item item = m_index.data(EntityTreeModel::ItemRole).value<Item>();
0094     if (!item.isValid()) {
0095         Collection col = m_index.data(EntityTreeModel::CollectionRole).value<Collection>();
0096         if (!col.isValid()) {
0097             return -1;
0098         }
0099         return col.id();
0100     }
0101     return item.id();
0102 }
0103 
0104 bool KJotsEntity::isBook() const
0105 {
0106     Collection col = m_index.data(EntityTreeModel::CollectionRole).value<Collection>();
0107 
0108     if (col.isValid()) {
0109         return col.contentMimeTypes().contains(Akonadi::NoteUtils::noteMimeType());
0110     }
0111     return false;
0112 }
0113 
0114 bool KJotsEntity::isPage() const
0115 {
0116     Item item = m_index.data(EntityTreeModel::ItemRole).value<Item>();
0117     if (item.isValid()) {
0118         return item.hasPayload<KMime::Message::Ptr>();
0119     }
0120     return false;
0121 }
0122 
0123 QVariantList KJotsEntity::entities() const
0124 {
0125     const QAbstractItemModel *model = m_index.model();
0126     QVariantList list;
0127     int row = 0;
0128     const int column = 0;
0129     QModelIndex childIndex = model->index(row++, column, m_index);
0130     while (childIndex.isValid()) {
0131         auto *obj = new KJotsEntity(childIndex);
0132         list << QVariant::fromValue(obj);
0133         childIndex = model->index(row++, column, m_index);
0134     }
0135     return list;
0136 }
0137 
0138 QVariantList KJotsEntity::breadcrumbs() const
0139 {
0140     QVariantList list;
0141     QModelIndex parent = m_index.parent();
0142 
0143     while (parent.isValid()) {
0144         QObject *obj = new KJotsEntity(parent);
0145         list << QVariant::fromValue(obj);
0146         parent = parent.parent();
0147     }
0148     return list;
0149 }
0150 
0151 KJotsModel::KJotsModel(ChangeRecorder *monitor, QObject *parent)
0152     : EntityTreeModel(monitor, parent)
0153 {
0154 
0155 }
0156 
0157 KJotsModel::~KJotsModel()
0158 {
0159     qDeleteAll(m_documents);
0160 }
0161 
0162 bool KJotsModel::setData(const QModelIndex &index, const QVariant &value, int role)
0163 {
0164     if (role == Qt::EditRole) {
0165         Item item = index.data(ItemRole).value<Item>();
0166 
0167         if (!item.isValid()) {
0168             Collection col = index.data(CollectionRole).value<Collection>();
0169             col.setName(value.toString());
0170             if (col.hasAttribute<EntityDisplayAttribute>()) {
0171                 auto *eda = col.attribute<EntityDisplayAttribute>();
0172                 eda->setDisplayName(value.toString());
0173             }
0174             return EntityTreeModel::setData(index, QVariant::fromValue(col), CollectionRole);
0175         }
0176         NoteUtils::NoteMessageWrapper note(item.payload<KMime::Message::Ptr>());
0177         note.setTitle(value.toString());
0178         item.setPayload(note.message());
0179 
0180         if (item.hasAttribute<EntityDisplayAttribute>()) {
0181             auto *displayAttribute = item.attribute<EntityDisplayAttribute>();
0182             displayAttribute->setDisplayName(value.toString());
0183         }
0184         return EntityTreeModel::setData(index, QVariant::fromValue(item), ItemRole);
0185     }
0186 
0187     if (role == KJotsModel::DocumentRole) {
0188         Item item = updateItem(index.data(ItemRole).value<Item>(), value.value<QTextDocument *>());
0189         return EntityTreeModel::setData(index, QVariant::fromValue(item), ItemRole);
0190     }
0191 
0192     return EntityTreeModel::setData(index, value, role);
0193 }
0194 
0195 QVariant KJotsModel::data(const QModelIndex &index, int role) const
0196 {
0197     if (GrantleeObjectRole == role) {
0198         auto obj = new KJotsEntity(index);
0199         obj->setIndex(index);
0200         return QVariant::fromValue(obj);
0201     }
0202 
0203     if (role == KJotsModel::DocumentRole) {
0204         const Item item = index.data(ItemRole).value<Item>();
0205         Item::Id itemId = item.id();
0206         if (m_documents.contains(itemId)) {
0207             return QVariant::fromValue(m_documents.value(itemId));
0208         }
0209         if (!item.hasPayload<KMime::Message::Ptr>()) {
0210             return QVariant();
0211         }
0212 
0213         NoteUtils::NoteMessageWrapper note(item.payload<KMime::Message::Ptr>());
0214         const QString doc = note.text();
0215         auto document = new QTextDocument();
0216         if (note.textFormat() == Qt::RichText
0217                 || doc.startsWith(u"<!DOCTYPE")
0218                 || doc.startsWith(u"<html>"))
0219         {
0220             document->setHtml(doc);
0221         } else {
0222             document->setPlainText(doc);
0223         }
0224         document->setModified(false);
0225         // Loading embedded images
0226         const QVector<NoteUtils::Attachment> attachments = note.attachments();
0227         for (const auto &attachment : attachments) {
0228             if (attachment.mimetype() == QStringLiteral("image/png") && !attachment.contentID().isEmpty()) {
0229                 const QImage img = QImage::fromData(attachment.data(), "PNG");
0230                 document->addResource(QTextDocument::ImageResource,
0231                                       QUrl(QStringLiteral("cid:")+attachment.contentID()), img);
0232             }
0233         }
0234 
0235         m_documents.insert(itemId, document);
0236         return QVariant::fromValue(document);
0237     }
0238 
0239     if (role == Qt::DecorationRole && index.column() == Title) {
0240         const Item item = index.data(ItemRole).value<Item>();
0241         if (item.isValid() && item.hasAttribute<NoteShared::NoteLockAttribute>()) {
0242             return QIcon::fromTheme(QStringLiteral("object-locked"));
0243         }
0244         if (item.isValid() && item.hasAttribute<NoteShared::NotePinAttribute>()) {
0245             return QIcon::fromTheme(QStringLiteral("pin"));
0246         }
0247         const Collection col = index.data(CollectionRole).value<Collection>();
0248         if (col.isValid() && col.hasAttribute<NoteShared::NoteLockAttribute>()) {
0249             return QIcon::fromTheme(QStringLiteral("object-locked"));
0250         }
0251     }
0252 
0253     return EntityTreeModel::data(index, role);
0254 }
0255 
0256 QVariant KJotsModel::entityData(const Akonadi::Item &item, int column, int role) const
0257 {
0258     if (item.hasPayload<KMime::Message::Ptr>()) {
0259         auto message = item.payload<KMime::Message::Ptr>();
0260         NoteUtils::NoteMessageWrapper note(message);
0261         if (role == Qt::DisplayRole) {
0262             switch (column) {
0263             case Title:
0264                 return note.title();
0265             case ModificationTime:
0266                 return KFormat().formatRelativeDateTime(note.lastModifiedDate(), QLocale::ShortFormat);
0267             case CreationTime:
0268                 return KFormat().formatRelativeDateTime(note.creationDate(), QLocale::ShortFormat);
0269             case Size:
0270                 return KFormat().formatByteSize(message->storageSize());
0271             }
0272         } else if (role == Qt::EditRole) {
0273             switch (column) {
0274             case Title:
0275                 return note.title();
0276             case ModificationTime:
0277                 return note.lastModifiedDate();
0278             case CreationTime:
0279                 return note.creationDate();
0280             case Size:
0281                 return message->size();
0282             }
0283 
0284         }
0285     }
0286     return EntityTreeModel::entityData(item, column, role);
0287 }
0288 
0289 QVariant KJotsModel::entityHeaderData(int section, Qt::Orientation orientation, int role, HeaderGroup headerGroup) const
0290 {
0291     if (role != Qt::DisplayRole || orientation != Qt::Horizontal) {
0292         return EntityTreeModel:: entityHeaderData(section, orientation, role, headerGroup);
0293     }
0294     if (headerGroup == EntityTreeModel::CollectionTreeHeaders) {
0295         return i18nc("@title:column", "Name");
0296     } else if (headerGroup == EntityTreeModel::ItemListHeaders) {
0297         switch (section) {
0298         case Title:
0299             return i18nc("@title:column title of a note", "Title");
0300         case CreationTime:
0301             return i18nc("@title:column creation date and time of a note", "Created");
0302         case ModificationTime:
0303             return i18nc("@title:column last modification date and time of a note", "Modified");
0304         case Size:
0305             return i18nc("@title:column size of a note", "Size");
0306         }
0307     }
0308     return EntityTreeModel::entityHeaderData(section, orientation, role, headerGroup);
0309 }
0310 
0311 int KJotsModel::entityColumnCount(HeaderGroup headerGroup) const
0312 {
0313     if (headerGroup == EntityTreeModel::CollectionTreeHeaders) {
0314         return 1;
0315     } else if (headerGroup == EntityTreeModel::ItemListHeaders) {
0316         return 4;
0317     } else {
0318         return EntityTreeModel::entityColumnCount(headerGroup);
0319     }
0320 }
0321 
0322 QModelIndex KJotsModel::modelIndexForUrl(const QAbstractItemModel *model, const QUrl &url)
0323 {
0324     if (url.scheme() != QStringLiteral("akonadi")) {
0325         return {};
0326     }
0327     const auto item = Item::fromUrl(url);
0328     const auto col = Collection::fromUrl(url);
0329     if (item.isValid()) {
0330         const QModelIndexList idxs = EntityTreeModel::modelIndexesForItem(model, item);
0331         if (!idxs.isEmpty()) {
0332             return idxs.first();
0333         }
0334     } else if (col.isValid()) {
0335         return EntityTreeModel::modelIndexForCollection(model, col);
0336     }
0337     return {};
0338 }
0339 
0340 Item KJotsModel::updateItem(const Item &item, QTextDocument *document)
0341 {
0342     if (!item.hasPayload<KMime::Message::Ptr>()) {
0343         return {};
0344     }
0345     NoteUtils::NoteMessageWrapper note(item.payload<KMime::Message::Ptr>());
0346     // Saving embedded images
0347     const auto images = document->property("images").value<KPIMTextEdit::ImageList>();
0348     QVector<NoteUtils::Attachment> &attachments = note.attachments();
0349     attachments.clear();
0350     attachments.reserve(images.count());
0351     std::transform(images.cbegin(), images.cend(), std::back_inserter(attachments),
0352                    [](const QSharedPointer<KPIMTextEdit::EmbeddedImage> &img) {
0353             NoteUtils::Attachment attachment(img->image, QStringLiteral("image/png"));
0354             attachment.setDataBase64Encoded(true);
0355             attachment.setContentID(img->contentID);
0356             return attachment;
0357         });
0358     // Setting text
0359     bool isRichText = KPIMTextEdit::TextUtils::containsFormatting(document);
0360     if (isRichText) {
0361         const QByteArray html = KPIMTextEdit::RichTextComposerImages::imageNamesToContentIds( document->toHtml().toUtf8(), images);
0362         note.setText( QString::fromUtf8(html), Qt::RichText );
0363     } else {
0364         note.setText( document->toPlainText(), Qt::PlainText );
0365     }
0366     note.setLastModifiedDate(QDateTime::currentDateTime());
0367 
0368     Item newItem = item;
0369     newItem.setPayload(note.message());
0370     return newItem;
0371 }
0372 
0373 QString KJotsModel::itemPath(const QModelIndex &index, const QString &sep)
0374 {
0375     QStringList path;
0376     QModelIndex curIndex = index;
0377     while (curIndex.isValid()) {
0378         path.prepend(curIndex.data().toString());
0379         curIndex = curIndex.parent();
0380     }
0381     return path.join(sep);
0382 }
0383 
0384 QModelIndex KJotsModel::etmIndex(const QModelIndex &index)
0385 {
0386     QModelIndex result = index;
0387     const QAbstractProxyModel *proxy = qobject_cast<const QAbstractProxyModel *>(index.model());
0388     while (proxy) {
0389         result = proxy->mapToSource(result);
0390         proxy = qobject_cast<const QAbstractProxyModel *>(result.model());
0391     }
0392     return result;
0393 }
0394 
0395 #include "moc_kjotsmodel.cpp"