File indexing completed on 2024-12-15 04:54:38
0001 /****************************************************************************** 0002 * 0003 * SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net> 0004 * 0005 * SPDX-License-Identifier: GPL-2.0-or-later 0006 * 0007 *******************************************************************************/ 0008 0009 #include "messageitem.h" 0010 #include "messageitem_p.h" 0011 0012 #include "messagelist_debug.h" 0013 #include <Akonadi/EntityAnnotationsAttribute> 0014 #include <Akonadi/Item> 0015 #include <Akonadi/TagAttribute> 0016 #include <Akonadi/TagFetchJob> 0017 #include <Akonadi/TagFetchScope> 0018 #include <KIconLoader> 0019 #include <KLocalizedString> 0020 #include <QIcon> 0021 #include <QPointer> 0022 using namespace MessageList::Core; 0023 0024 Q_GLOBAL_STATIC(TagCache, s_tagCache) 0025 0026 class MessageItem::Tag::TagPrivate 0027 { 0028 public: 0029 TagPrivate() 0030 : mPriority(0) // Initialize it 0031 { 0032 } 0033 0034 QPixmap mPixmap; 0035 QString mName; 0036 QString mId; ///< The unique id of this tag 0037 QColor mTextColor; 0038 QColor mBackgroundColor; 0039 QFont mFont; 0040 int mPriority; 0041 }; 0042 0043 MessageItem::Tag::Tag(const QPixmap &pix, const QString &tagName, const QString &tagId) 0044 : d(new TagPrivate) 0045 { 0046 d->mPixmap = pix; 0047 d->mName = tagName; 0048 d->mId = tagId; 0049 } 0050 0051 MessageItem::Tag::~Tag() = default; 0052 0053 const QPixmap &MessageItem::Tag::pixmap() const 0054 { 0055 return d->mPixmap; 0056 } 0057 0058 const QString &MessageItem::Tag::name() const 0059 { 0060 return d->mName; 0061 } 0062 0063 const QString &MessageItem::Tag::id() const 0064 { 0065 return d->mId; 0066 } 0067 0068 const QColor &MessageItem::Tag::textColor() const 0069 { 0070 return d->mTextColor; 0071 } 0072 0073 const QColor &MessageItem::Tag::backgroundColor() const 0074 { 0075 return d->mBackgroundColor; 0076 } 0077 0078 const QFont &MessageItem::Tag::font() const 0079 { 0080 return d->mFont; 0081 } 0082 0083 int MessageItem::Tag::priority() const 0084 { 0085 return d->mPriority; 0086 } 0087 0088 void MessageItem::Tag::setTextColor(const QColor &textColor) 0089 { 0090 d->mTextColor = textColor; 0091 } 0092 0093 void MessageItem::Tag::setBackgroundColor(const QColor &backgroundColor) 0094 { 0095 d->mBackgroundColor = backgroundColor; 0096 } 0097 0098 void MessageItem::Tag::setFont(const QFont &font) 0099 { 0100 d->mFont = font; 0101 } 0102 0103 void MessageItem::Tag::setPriority(int priority) 0104 { 0105 d->mPriority = priority; 0106 } 0107 0108 class MessageItemPrivateSettings 0109 { 0110 public: 0111 QColor mColorUnreadMessage; 0112 QColor mColorImportantMessage; 0113 QColor mColorToDoMessage; 0114 QFont mFont; 0115 QFont mFontUnreadMessage; 0116 QFont mFontImportantMessage; 0117 QFont mFontToDoMessage; 0118 0119 // Keep those two invalid. They are here purely so that MessageItem can return 0120 // const reference to them 0121 QColor mColor; 0122 QColor mBackgroundColor; 0123 }; 0124 0125 Q_GLOBAL_STATIC(MessageItemPrivateSettings, s_settings) 0126 0127 MessageItemPrivate::MessageItemPrivate(MessageItem *qq) 0128 : ItemPrivate(qq) 0129 , mThreadingStatus(MessageItem::ParentMissing) 0130 , mEncryptionState(MessageItem::NotEncrypted) 0131 , mSignatureState(MessageItem::NotSigned) 0132 , mAboutToBeRemoved(false) 0133 , mSubjectIsPrefixed(false) 0134 , mTagList(nullptr) 0135 { 0136 } 0137 0138 MessageItemPrivate::~MessageItemPrivate() 0139 { 0140 s_tagCache->cancelRequest(this); 0141 invalidateTagCache(); 0142 } 0143 0144 void MessageItemPrivate::invalidateTagCache() 0145 { 0146 if (mTagList) { 0147 qDeleteAll(*mTagList); 0148 delete mTagList; 0149 mTagList = nullptr; 0150 } 0151 } 0152 0153 void MessageItemPrivate::invalidateAnnotationCache() 0154 { 0155 } 0156 0157 const MessageItem::Tag *MessageItemPrivate::bestTag() const 0158 { 0159 const MessageItem::Tag *best = nullptr; 0160 const auto tagList{getTagList()}; 0161 for (const MessageItem::Tag *tag : tagList) { 0162 if (!best || tag->priority() < best->priority()) { 0163 best = tag; 0164 } 0165 } 0166 return best; 0167 } 0168 0169 void MessageItemPrivate::fillTagList(const Akonadi::Tag::List &taglist) 0170 { 0171 Q_ASSERT(!mTagList); 0172 mTagList = new QList<MessageItem::Tag *>; 0173 0174 // TODO: The tag pointers here could be shared between all items, there really is no point in 0175 // creating them for each item that has tags 0176 0177 // Priority sort this and make bestTag more efficient 0178 0179 for (const Akonadi::Tag &tag : taglist) { 0180 QString symbol = QStringLiteral("mail-tagged"); 0181 const auto attr = tag.attribute<Akonadi::TagAttribute>(); 0182 if (attr) { 0183 if (!attr->iconName().isEmpty()) { 0184 symbol = attr->iconName(); 0185 } 0186 } 0187 auto messageListTag = new MessageItem::Tag(QIcon::fromTheme(symbol).pixmap(KIconLoader::SizeSmall), tag.name(), tag.url().url()); 0188 0189 if (attr) { 0190 messageListTag->setTextColor(attr->textColor()); 0191 messageListTag->setBackgroundColor(attr->backgroundColor()); 0192 if (!attr->font().isEmpty()) { 0193 QFont font; 0194 if (font.fromString(attr->font())) { 0195 messageListTag->setFont(font); 0196 } 0197 } 0198 if (attr->priority() != -1) { 0199 messageListTag->setPriority(attr->priority()); 0200 } else { 0201 messageListTag->setPriority(0xFFFF); 0202 } 0203 } 0204 0205 mTagList->append(messageListTag); 0206 } 0207 } 0208 0209 QList<MessageItem::Tag *> MessageItemPrivate::getTagList() const 0210 { 0211 if (!mTagList) { 0212 s_tagCache->retrieveTags(mAkonadiItem.tags(), const_cast<MessageItemPrivate *>(this)); 0213 return {}; 0214 } 0215 0216 return *mTagList; 0217 } 0218 0219 bool MessageItemPrivate::tagListInitialized() const 0220 { 0221 return mTagList != nullptr; 0222 } 0223 0224 MessageItem::MessageItem() 0225 : Item(Message, new MessageItemPrivate(this)) 0226 , ModelInvariantIndex() 0227 { 0228 } 0229 0230 MessageItem::MessageItem(MessageItemPrivate *dd) 0231 : Item(Message, dd) 0232 , ModelInvariantIndex() 0233 { 0234 } 0235 0236 MessageItem::~MessageItem() = default; 0237 0238 QList<MessageItem::Tag *> MessageItem::tagList() const 0239 { 0240 Q_D(const MessageItem); 0241 return d->getTagList(); 0242 } 0243 0244 bool MessageItem::hasAnnotation() const 0245 { 0246 Q_D(const MessageItem); 0247 // TODO check for note entry? 0248 return d->mAkonadiItem.hasAttribute<Akonadi::EntityAnnotationsAttribute>(); 0249 } 0250 0251 QString MessageItem::annotation() const 0252 { 0253 Q_D(const MessageItem); 0254 if (d->mAkonadiItem.hasAttribute<Akonadi::EntityAnnotationsAttribute>()) { 0255 auto attr = d->mAkonadiItem.attribute<Akonadi::EntityAnnotationsAttribute>(); 0256 const auto annotations = attr->annotations(); 0257 QByteArray annot = annotations.value("/private/comment"); 0258 if (!annot.isEmpty()) { 0259 return QString::fromLatin1(annot); 0260 } 0261 annot = annotations.value("/shared/comment"); 0262 if (!annot.isEmpty()) { 0263 return QString::fromLatin1(annot); 0264 } 0265 } 0266 return {}; 0267 } 0268 0269 void MessageItem::editAnnotation(QWidget *parent) 0270 { 0271 Q_D(MessageItem); 0272 QPointer<PimCommon::AnnotationEditDialog> mAnnotationDialog = new PimCommon::AnnotationEditDialog(d->mAkonadiItem, parent); 0273 // FIXME make async 0274 if (mAnnotationDialog->exec()) { 0275 // invalidate the cached mHasAnnotation value 0276 } 0277 delete mAnnotationDialog; 0278 } 0279 0280 const MessageItem::Tag *MessageItemPrivate::findTagInternal(const QString &szTagId) const 0281 { 0282 const auto tagList{getTagList()}; 0283 for (const MessageItem::Tag *tag : tagList) { 0284 if (tag->id() == szTagId) { 0285 return tag; 0286 } 0287 } 0288 return nullptr; 0289 } 0290 0291 const MessageItem::Tag *MessageItem::findTag(const QString &szTagId) const 0292 { 0293 Q_D(const MessageItem); 0294 return d->findTagInternal(szTagId); 0295 } 0296 0297 QString MessageItem::tagListDescription() const 0298 { 0299 QString ret; 0300 0301 const auto tags{tagList()}; 0302 for (const Tag *tag : tags) { 0303 if (!ret.isEmpty()) { 0304 ret += QLatin1StringView(", "); 0305 } 0306 ret += tag->name(); 0307 } 0308 0309 return ret; 0310 } 0311 0312 void MessageItem::invalidateTagCache() 0313 { 0314 Q_D(MessageItem); 0315 d->invalidateTagCache(); 0316 } 0317 0318 void MessageItem::invalidateAnnotationCache() 0319 { 0320 Q_D(MessageItem); 0321 d->invalidateAnnotationCache(); 0322 } 0323 0324 const QColor &MessageItem::textColor() const 0325 { 0326 Q_D(const MessageItem); 0327 const Tag *bestTag = d->bestTag(); 0328 if (bestTag != nullptr && bestTag->textColor().isValid()) { 0329 return bestTag->textColor(); 0330 } 0331 0332 Akonadi::MessageStatus messageStatus = status(); 0333 if (!messageStatus.isRead()) { 0334 return s_settings->mColorUnreadMessage; 0335 } else if (messageStatus.isImportant()) { 0336 return s_settings->mColorImportantMessage; 0337 } else if (messageStatus.isToAct()) { 0338 return s_settings->mColorToDoMessage; 0339 } else { 0340 return s_settings->mColor; 0341 } 0342 } 0343 0344 const QColor &MessageItem::backgroundColor() const 0345 { 0346 Q_D(const MessageItem); 0347 const Tag *bestTag = d->bestTag(); 0348 if (bestTag) { 0349 return bestTag->backgroundColor(); 0350 } else { 0351 return s_settings->mBackgroundColor; 0352 } 0353 } 0354 0355 const QFont &MessageItem::font() const 0356 { 0357 Q_D(const MessageItem); 0358 // for performance reasons we don't want font retrieval to trigger 0359 // full tags loading, as the font is used for geometry calculation 0360 // and thus this method called for each item 0361 if (d->tagListInitialized()) { 0362 const Tag *bestTag = d->bestTag(); 0363 if (bestTag && bestTag->font() != QFont()) { 0364 return bestTag->font(); 0365 } 0366 } 0367 0368 // from KDE3: "important" overrides "new" overrides "unread" overrides "todo" 0369 Akonadi::MessageStatus messageStatus = status(); 0370 if (messageStatus.isImportant()) { 0371 return s_settings->mFontImportantMessage; 0372 } else if (!messageStatus.isRead()) { 0373 return s_settings->mFontUnreadMessage; 0374 } else if (messageStatus.isToAct()) { 0375 return s_settings->mFontToDoMessage; 0376 } else { 0377 return s_settings->mFont; 0378 } 0379 } 0380 0381 MessageItem::SignatureState MessageItem::signatureState() const 0382 { 0383 Q_D(const MessageItem); 0384 return d->mSignatureState; 0385 } 0386 0387 void MessageItem::setSignatureState(SignatureState state) 0388 { 0389 Q_D(MessageItem); 0390 d->mSignatureState = state; 0391 } 0392 0393 MessageItem::EncryptionState MessageItem::encryptionState() const 0394 { 0395 Q_D(const MessageItem); 0396 return d->mEncryptionState; 0397 } 0398 0399 void MessageItem::setEncryptionState(EncryptionState state) 0400 { 0401 Q_D(MessageItem); 0402 d->mEncryptionState = state; 0403 } 0404 0405 QByteArray MessageItem::messageIdMD5() const 0406 { 0407 Q_D(const MessageItem); 0408 return d->mMessageIdMD5; 0409 } 0410 0411 void MessageItem::setMessageIdMD5(const QByteArray &md5) 0412 { 0413 Q_D(MessageItem); 0414 d->mMessageIdMD5 = md5; 0415 } 0416 0417 QByteArray MessageItem::inReplyToIdMD5() const 0418 { 0419 Q_D(const MessageItem); 0420 return d->mInReplyToIdMD5; 0421 } 0422 0423 void MessageItem::setInReplyToIdMD5(const QByteArray &md5) 0424 { 0425 Q_D(MessageItem); 0426 d->mInReplyToIdMD5 = md5; 0427 } 0428 0429 QByteArray MessageItem::referencesIdMD5() const 0430 { 0431 Q_D(const MessageItem); 0432 return d->mReferencesIdMD5; 0433 } 0434 0435 void MessageItem::setReferencesIdMD5(const QByteArray &md5) 0436 { 0437 Q_D(MessageItem); 0438 d->mReferencesIdMD5 = md5; 0439 } 0440 0441 void MessageItem::setSubjectIsPrefixed(bool subjectIsPrefixed) 0442 { 0443 Q_D(MessageItem); 0444 d->mSubjectIsPrefixed = subjectIsPrefixed; 0445 } 0446 0447 bool MessageItem::subjectIsPrefixed() const 0448 { 0449 Q_D(const MessageItem); 0450 return d->mSubjectIsPrefixed; 0451 } 0452 0453 QByteArray MessageItem::strippedSubjectMD5() const 0454 { 0455 Q_D(const MessageItem); 0456 return d->mStrippedSubjectMD5; 0457 } 0458 0459 void MessageItem::setStrippedSubjectMD5(const QByteArray &md5) 0460 { 0461 Q_D(MessageItem); 0462 d->mStrippedSubjectMD5 = md5; 0463 } 0464 0465 bool MessageItem::aboutToBeRemoved() const 0466 { 0467 Q_D(const MessageItem); 0468 return d->mAboutToBeRemoved; 0469 } 0470 0471 void MessageItem::setAboutToBeRemoved(bool aboutToBeRemoved) 0472 { 0473 Q_D(MessageItem); 0474 d->mAboutToBeRemoved = aboutToBeRemoved; 0475 } 0476 0477 MessageItem::ThreadingStatus MessageItem::threadingStatus() const 0478 { 0479 Q_D(const MessageItem); 0480 return d->mThreadingStatus; 0481 } 0482 0483 void MessageItem::setThreadingStatus(ThreadingStatus threadingStatus) 0484 { 0485 Q_D(MessageItem); 0486 d->mThreadingStatus = threadingStatus; 0487 } 0488 0489 unsigned long MessageItem::uniqueId() const 0490 { 0491 Q_D(const MessageItem); 0492 return d->mAkonadiItem.id(); 0493 } 0494 0495 Akonadi::Item MessageList::Core::MessageItem::akonadiItem() const 0496 { 0497 Q_D(const MessageItem); 0498 return d->mAkonadiItem; 0499 } 0500 0501 void MessageList::Core::MessageItem::setAkonadiItem(const Akonadi::Item &item) 0502 { 0503 Q_D(MessageItem); 0504 d->mAkonadiItem = item; 0505 } 0506 0507 MessageItem *MessageItem::topmostMessage() 0508 { 0509 if (!parent()) { 0510 return this; 0511 } 0512 if (parent()->type() == Item::Message) { 0513 return static_cast<MessageItem *>(parent())->topmostMessage(); 0514 } 0515 return this; 0516 } 0517 0518 QString MessageItem::accessibleTextForField(Theme::ContentItem::Type field) 0519 { 0520 switch (field) { 0521 case Theme::ContentItem::Subject: 0522 return d_ptr->mSubject; 0523 case Theme::ContentItem::Sender: 0524 return d_ptr->mSender; 0525 case Theme::ContentItem::Receiver: 0526 return d_ptr->mReceiver; 0527 case Theme::ContentItem::SenderOrReceiver: 0528 return senderOrReceiver(); 0529 case Theme::ContentItem::Date: 0530 return formattedDate(); 0531 case Theme::ContentItem::Size: 0532 return formattedSize(); 0533 case Theme::ContentItem::RepliedStateIcon: 0534 return status().isReplied() ? i18nc("Status of an item", "Replied") : QString(); 0535 case Theme::ContentItem::ReadStateIcon: 0536 return status().isRead() ? i18nc("Status of an item", "Read") : i18nc("Status of an item", "Unread"); 0537 case Theme::ContentItem::CombinedReadRepliedStateIcon: 0538 return accessibleTextForField(Theme::ContentItem::ReadStateIcon) + accessibleTextForField(Theme::ContentItem::RepliedStateIcon); 0539 default: 0540 return {}; 0541 } 0542 } 0543 0544 QString MessageItem::accessibleText(const Theme *theme, int columnIndex) 0545 { 0546 QStringList rowsTexts; 0547 const QList<Theme::Row *> rows = theme->column(columnIndex)->messageRows(); 0548 rowsTexts.reserve(rows.count()); 0549 0550 for (Theme::Row *row : rows) { 0551 QStringList leftStrings; 0552 QStringList rightStrings; 0553 const auto leftItems = row->leftItems(); 0554 leftStrings.reserve(leftItems.count()); 0555 for (Theme::ContentItem *contentItem : std::as_const(leftItems)) { 0556 leftStrings.append(accessibleTextForField(contentItem->type())); 0557 } 0558 0559 const auto rightItems = row->rightItems(); 0560 rightStrings.reserve(rightItems.count()); 0561 for (Theme::ContentItem *contentItem : rightItems) { 0562 rightStrings.insert(rightStrings.begin(), accessibleTextForField(contentItem->type())); 0563 } 0564 0565 rowsTexts.append((leftStrings + rightStrings).join(QLatin1Char(' '))); 0566 } 0567 0568 return rowsTexts.join(QLatin1Char(' ')); 0569 } 0570 0571 void MessageItem::subTreeToList(QList<MessageItem *> &list) 0572 { 0573 list.append(this); 0574 const auto childList = childItems(); 0575 if (!childList) { 0576 return; 0577 } 0578 for (const auto child : std::as_const(*childList)) { 0579 Q_ASSERT(child->type() == Item::Message); 0580 static_cast<MessageItem *>(child)->subTreeToList(list); 0581 } 0582 } 0583 0584 void MessageItem::setUnreadMessageColor(const QColor &color) 0585 { 0586 s_settings->mColorUnreadMessage = color; 0587 } 0588 0589 void MessageItem::setImportantMessageColor(const QColor &color) 0590 { 0591 s_settings->mColorImportantMessage = color; 0592 } 0593 0594 void MessageItem::setToDoMessageColor(const QColor &color) 0595 { 0596 s_settings->mColorToDoMessage = color; 0597 } 0598 0599 void MessageItem::setGeneralFont(const QFont &font) 0600 { 0601 s_settings->mFont = font; 0602 } 0603 0604 void MessageItem::setUnreadMessageFont(const QFont &font) 0605 { 0606 s_settings->mFontUnreadMessage = font; 0607 } 0608 0609 void MessageItem::setImportantMessageFont(const QFont &font) 0610 { 0611 s_settings->mFontImportantMessage = font; 0612 } 0613 0614 void MessageItem::setToDoMessageFont(const QFont &font) 0615 { 0616 s_settings->mFontToDoMessage = font; 0617 } 0618 0619 FakeItemPrivate::FakeItemPrivate(FakeItem *qq) 0620 : MessageItemPrivate(qq) 0621 { 0622 } 0623 0624 FakeItem::FakeItem() 0625 : MessageItem(new FakeItemPrivate(this)) 0626 { 0627 } 0628 0629 FakeItem::~FakeItem() 0630 { 0631 Q_D(const FakeItem); 0632 qDeleteAll(d->mFakeTags); 0633 } 0634 0635 QList<MessageItem::Tag *> FakeItem::tagList() const 0636 { 0637 Q_D(const FakeItem); 0638 return d->mFakeTags; 0639 } 0640 0641 void FakeItem::setFakeTags(const QList<MessageItem::Tag *> &tagList) 0642 { 0643 Q_D(FakeItem); 0644 d->mFakeTags = tagList; 0645 } 0646 0647 bool FakeItem::hasAnnotation() const 0648 { 0649 return true; 0650 } 0651 0652 TagCache::TagCache() 0653 : QObject() 0654 , mMonitor(new Akonadi::Monitor(this)) 0655 { 0656 mCache.setMaxCost(100); 0657 mMonitor->setObjectName(QLatin1StringView("MessageListTagCacheMonitor")); 0658 mMonitor->setTypeMonitored(Akonadi::Monitor::Tags); 0659 mMonitor->tagFetchScope().fetchAttribute<Akonadi::TagAttribute>(); 0660 connect(mMonitor, &Akonadi::Monitor::tagAdded, this, &TagCache::onTagAdded); 0661 connect(mMonitor, &Akonadi::Monitor::tagRemoved, this, &TagCache::onTagRemoved); 0662 connect(mMonitor, &Akonadi::Monitor::tagChanged, this, &TagCache::onTagChanged); 0663 } 0664 0665 void TagCache::onTagAdded(const Akonadi::Tag &tag) 0666 { 0667 mCache.insert(tag.id(), new Akonadi::Tag(tag)); 0668 } 0669 0670 void TagCache::onTagChanged(const Akonadi::Tag &tag) 0671 { 0672 mCache.remove(tag.id()); 0673 } 0674 0675 void TagCache::onTagRemoved(const Akonadi::Tag &tag) 0676 { 0677 mCache.remove(tag.id()); 0678 } 0679 0680 void TagCache::retrieveTags(const Akonadi::Tag::List &tags, MessageItemPrivate *m) 0681 { 0682 // Retrieval is in progress 0683 if (mRequests.key(m)) { 0684 return; 0685 } 0686 Akonadi::Tag::List toFetch; 0687 Akonadi::Tag::List available; 0688 for (const Akonadi::Tag &tag : tags) { 0689 if (mCache.contains(tag.id())) { 0690 available << *mCache.object(tag.id()); 0691 } else { 0692 toFetch << tag; 0693 } 0694 } 0695 // Because fillTagList expects to be called once we either fetch all or none 0696 if (!toFetch.isEmpty()) { 0697 auto tagFetchJob = new Akonadi::TagFetchJob(tags, this); 0698 tagFetchJob->fetchScope().fetchAttribute<Akonadi::TagAttribute>(); 0699 connect(tagFetchJob, &Akonadi::TagFetchJob::result, this, &TagCache::onTagsFetched); 0700 mRequests.insert(tagFetchJob, m); 0701 } else { 0702 m->fillTagList(available); 0703 } 0704 } 0705 0706 void TagCache::cancelRequest(MessageItemPrivate *m) 0707 { 0708 const QList<KJob *> keys = mRequests.keys(m); 0709 for (KJob *job : keys) { 0710 mRequests.remove(job); 0711 } 0712 } 0713 0714 void TagCache::onTagsFetched(KJob *job) 0715 { 0716 if (job->error()) { 0717 qCWarning(MESSAGELIST_LOG) << "Failed to fetch tags: " << job->errorString(); 0718 return; 0719 } 0720 auto fetchJob = static_cast<Akonadi::TagFetchJob *>(job); 0721 const auto tags{fetchJob->tags()}; 0722 for (const Akonadi::Tag &tag : tags) { 0723 mCache.insert(tag.id(), new Akonadi::Tag(tag)); 0724 } 0725 if (auto m = mRequests.take(fetchJob)) { 0726 m->fillTagList(fetchJob->tags()); 0727 } 0728 } 0729 0730 #include "moc_messageitem_p.cpp"