Messagelib

messageitem.cpp
1/******************************************************************************
2 *
3 * SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 *
7 *******************************************************************************/
8
9#include "messageitem.h"
10#include "messageitem_p.h"
11
12#include "messagelist_debug.h"
13#include <Akonadi/EntityAnnotationsAttribute>
14#include <Akonadi/Item>
15#include <Akonadi/TagAttribute>
16#include <Akonadi/TagFetchJob>
17#include <Akonadi/TagFetchScope>
18#include <KIconLoader>
19#include <KLocalizedString>
20#include <QIcon>
21#include <QPointer>
22using namespace MessageList::Core;
23
24Q_GLOBAL_STATIC(TagCache, s_tagCache)
25
26class MessageItem::Tag::TagPrivate
27{
28public:
29 TagPrivate()
30 : mPriority(0) // Initialize it
31 {
32 }
33
34 QPixmap mPixmap;
35 QString mName;
36 QString mId; ///< The unique id of this tag
37 QColor mTextColor;
38 QColor mBackgroundColor;
39 QFont mFont;
40 int mPriority;
41};
42
43MessageItem::Tag::Tag(const QPixmap &pix, const QString &tagName, const QString &tagId)
44 : d(new TagPrivate)
45{
46 d->mPixmap = pix;
47 d->mName = tagName;
48 d->mId = tagId;
49}
50
51MessageItem::Tag::~Tag() = default;
52
53const QPixmap &MessageItem::Tag::pixmap() const
54{
55 return d->mPixmap;
56}
57
58const QString &MessageItem::Tag::name() const
59{
60 return d->mName;
61}
62
63const QString &MessageItem::Tag::id() const
64{
65 return d->mId;
66}
67
68const QColor &MessageItem::Tag::textColor() const
69{
70 return d->mTextColor;
71}
72
73const QColor &MessageItem::Tag::backgroundColor() const
74{
75 return d->mBackgroundColor;
76}
77
78const QFont &MessageItem::Tag::font() const
79{
80 return d->mFont;
81}
82
83int MessageItem::Tag::priority() const
84{
85 return d->mPriority;
86}
87
88void MessageItem::Tag::setTextColor(const QColor &textColor)
89{
90 d->mTextColor = textColor;
91}
92
93void MessageItem::Tag::setBackgroundColor(const QColor &backgroundColor)
94{
95 d->mBackgroundColor = backgroundColor;
96}
97
98void MessageItem::Tag::setFont(const QFont &font)
99{
100 d->mFont = font;
101}
102
103void MessageItem::Tag::setPriority(int priority)
104{
105 d->mPriority = priority;
106}
107
108class MessageItemPrivateSettings
109{
110public:
111 QColor mColorUnreadMessage;
112 QColor mColorImportantMessage;
113 QColor mColorToDoMessage;
114 QFont mFont;
115 QFont mFontUnreadMessage;
116 QFont mFontImportantMessage;
117 QFont mFontToDoMessage;
118
119 // Keep those two invalid. They are here purely so that MessageItem can return
120 // const reference to them
121 QColor mColor;
122 QColor mBackgroundColor;
123};
124
125Q_GLOBAL_STATIC(MessageItemPrivateSettings, s_settings)
126
127MessageItemPrivate::MessageItemPrivate(MessageItem *qq)
128 : ItemPrivate(qq)
129 , mThreadingStatus(MessageItem::ParentMissing)
130 , mEncryptionState(MessageItem::NotEncrypted)
131 , mSignatureState(MessageItem::NotSigned)
132 , mAboutToBeRemoved(false)
133 , mSubjectIsPrefixed(false)
134 , mTagList(nullptr)
135{
136}
137
138MessageItemPrivate::~MessageItemPrivate()
139{
140 s_tagCache->cancelRequest(this);
141 invalidateTagCache();
142}
143
144void MessageItemPrivate::invalidateTagCache()
145{
146 if (mTagList) {
147 qDeleteAll(*mTagList);
148 delete mTagList;
149 mTagList = nullptr;
150 }
151}
152
153void MessageItemPrivate::invalidateAnnotationCache()
154{
155}
156
157const MessageItem::Tag *MessageItemPrivate::bestTag() const
158{
159 const MessageItem::Tag *best = nullptr;
160 const auto tagList{getTagList()};
161 for (const MessageItem::Tag *tag : tagList) {
162 if (!best || tag->priority() < best->priority()) {
163 best = tag;
164 }
165 }
166 return best;
167}
168
169void MessageItemPrivate::fillTagList(const Akonadi::Tag::List &taglist)
170{
171 Q_ASSERT(!mTagList);
172 mTagList = new QList<MessageItem::Tag *>;
173
174 // TODO: The tag pointers here could be shared between all items, there really is no point in
175 // creating them for each item that has tags
176
177 // Priority sort this and make bestTag more efficient
178
179 for (const Akonadi::Tag &tag : taglist) {
180 QString symbol = QStringLiteral("mail-tagged");
181 const auto attr = tag.attribute<Akonadi::TagAttribute>();
182 if (attr) {
183 if (!attr->iconName().isEmpty()) {
184 symbol = attr->iconName();
185 }
186 }
187 auto messageListTag = new MessageItem::Tag(QIcon::fromTheme(symbol).pixmap(KIconLoader::SizeSmall), tag.name(), tag.url().url());
188
189 if (attr) {
190 messageListTag->setTextColor(attr->textColor());
191 messageListTag->setBackgroundColor(attr->backgroundColor());
192 if (!attr->font().isEmpty()) {
193 QFont font;
194 if (font.fromString(attr->font())) {
195 messageListTag->setFont(font);
196 }
197 }
198 if (attr->priority() != -1) {
199 messageListTag->setPriority(attr->priority());
200 } else {
201 messageListTag->setPriority(0xFFFF);
202 }
203 }
204
205 mTagList->append(messageListTag);
206 }
207}
208
209QList<MessageItem::Tag *> MessageItemPrivate::getTagList() const
210{
211 if (!mTagList) {
212 s_tagCache->retrieveTags(mAkonadiItem.tags(), const_cast<MessageItemPrivate *>(this));
213 return {};
214 }
215
216 return *mTagList;
217}
218
219bool MessageItemPrivate::tagListInitialized() const
220{
221 return mTagList != nullptr;
222}
223
224MessageItem::MessageItem()
225 : Item(Message, new MessageItemPrivate(this))
227{
228}
229
230MessageItem::MessageItem(MessageItemPrivate *dd)
231 : Item(Message, dd)
233{
234}
235
236MessageItem::~MessageItem() = default;
237
239{
240 Q_D(const MessageItem);
241 return d->getTagList();
242}
243
245{
246 Q_D(const MessageItem);
247 // TODO check for note entry?
248 return d->mAkonadiItem.hasAttribute<Akonadi::EntityAnnotationsAttribute>();
249}
250
252{
253 Q_D(const MessageItem);
254 if (d->mAkonadiItem.hasAttribute<Akonadi::EntityAnnotationsAttribute>()) {
255 auto attr = d->mAkonadiItem.attribute<Akonadi::EntityAnnotationsAttribute>();
256 const auto annotations = attr->annotations();
257 QByteArray annot = annotations.value("/private/comment");
258 if (!annot.isEmpty()) {
259 return QString::fromLatin1(annot);
260 }
261 annot = annotations.value("/shared/comment");
262 if (!annot.isEmpty()) {
263 return QString::fromLatin1(annot);
264 }
265 }
266 return {};
267}
268
270{
273 // FIXME make async
274 if (mAnnotationDialog->exec()) {
275 // invalidate the cached mHasAnnotation value
276 }
277 delete mAnnotationDialog;
278}
279
280const MessageItem::Tag *MessageItemPrivate::findTagInternal(const QString &szTagId) const
281{
282 const auto tagList{getTagList()};
283 for (const MessageItem::Tag *tag : tagList) {
284 if (tag->id() == szTagId) {
285 return tag;
286 }
287 }
288 return nullptr;
289}
290
291const MessageItem::Tag *MessageItem::findTag(const QString &szTagId) const
292{
293 Q_D(const MessageItem);
294 return d->findTagInternal(szTagId);
295}
296
297QString MessageItem::tagListDescription() const
298{
299 QString ret;
300
301 const auto tags{tagList()};
302 for (const Tag *tag : tags) {
303 if (!ret.isEmpty()) {
304 ret += QLatin1StringView(", ");
305 }
306 ret += tag->name();
307 }
308
309 return ret;
310}
311
313{
315 d->invalidateTagCache();
316}
317
319{
321 d->invalidateAnnotationCache();
322}
323
324const QColor &MessageItem::textColor() const
325{
326 Q_D(const MessageItem);
327 const Tag *bestTag = d->bestTag();
328 if (bestTag != nullptr && bestTag->textColor().isValid()) {
329 return bestTag->textColor();
330 }
331
332 Akonadi::MessageStatus messageStatus = status();
333 if (!messageStatus.isRead()) {
334 return s_settings->mColorUnreadMessage;
335 } else if (messageStatus.isImportant()) {
336 return s_settings->mColorImportantMessage;
337 } else if (messageStatus.isToAct()) {
338 return s_settings->mColorToDoMessage;
339 } else {
340 return s_settings->mColor;
341 }
342}
343
344const QColor &MessageItem::backgroundColor() const
345{
346 Q_D(const MessageItem);
347 const Tag *bestTag = d->bestTag();
348 if (bestTag) {
349 return bestTag->backgroundColor();
350 } else {
351 return s_settings->mBackgroundColor;
352 }
353}
354
355const QFont &MessageItem::font() const
356{
357 Q_D(const MessageItem);
358 // for performance reasons we don't want font retrieval to trigger
359 // full tags loading, as the font is used for geometry calculation
360 // and thus this method called for each item
361 if (d->tagListInitialized()) {
362 const Tag *bestTag = d->bestTag();
363 if (bestTag && bestTag->font() != QFont()) {
364 return bestTag->font();
365 }
366 }
367
368 // from KDE3: "important" overrides "new" overrides "unread" overrides "todo"
369 Akonadi::MessageStatus messageStatus = status();
370 if (messageStatus.isImportant()) {
371 return s_settings->mFontImportantMessage;
372 } else if (!messageStatus.isRead()) {
373 return s_settings->mFontUnreadMessage;
374 } else if (messageStatus.isToAct()) {
375 return s_settings->mFontToDoMessage;
376 } else {
377 return s_settings->mFont;
378 }
379}
380
381MessageItem::SignatureState MessageItem::signatureState() const
382{
383 Q_D(const MessageItem);
384 return d->mSignatureState;
385}
386
387void MessageItem::setSignatureState(SignatureState state)
388{
390 d->mSignatureState = state;
391}
392
393MessageItem::EncryptionState MessageItem::encryptionState() const
394{
395 Q_D(const MessageItem);
396 return d->mEncryptionState;
397}
398
399void MessageItem::setEncryptionState(EncryptionState state)
400{
402 d->mEncryptionState = state;
403}
404
405QByteArray MessageItem::messageIdMD5() const
406{
407 Q_D(const MessageItem);
408 return d->mMessageIdMD5;
409}
410
411void MessageItem::setMessageIdMD5(const QByteArray &md5)
412{
414 d->mMessageIdMD5 = md5;
415}
416
417QByteArray MessageItem::inReplyToIdMD5() const
418{
419 Q_D(const MessageItem);
420 return d->mInReplyToIdMD5;
421}
422
423void MessageItem::setInReplyToIdMD5(const QByteArray &md5)
424{
426 d->mInReplyToIdMD5 = md5;
427}
428
429QByteArray MessageItem::referencesIdMD5() const
430{
431 Q_D(const MessageItem);
432 return d->mReferencesIdMD5;
433}
434
435void MessageItem::setReferencesIdMD5(const QByteArray &md5)
436{
438 d->mReferencesIdMD5 = md5;
439}
440
441void MessageItem::setSubjectIsPrefixed(bool subjectIsPrefixed)
442{
444 d->mSubjectIsPrefixed = subjectIsPrefixed;
445}
446
447bool MessageItem::subjectIsPrefixed() const
448{
449 Q_D(const MessageItem);
450 return d->mSubjectIsPrefixed;
451}
452
453QByteArray MessageItem::strippedSubjectMD5() const
454{
455 Q_D(const MessageItem);
456 return d->mStrippedSubjectMD5;
457}
458
459void MessageItem::setStrippedSubjectMD5(const QByteArray &md5)
460{
462 d->mStrippedSubjectMD5 = md5;
463}
464
465bool MessageItem::aboutToBeRemoved() const
466{
467 Q_D(const MessageItem);
468 return d->mAboutToBeRemoved;
469}
470
471void MessageItem::setAboutToBeRemoved(bool aboutToBeRemoved)
472{
474 d->mAboutToBeRemoved = aboutToBeRemoved;
475}
476
477MessageItem::ThreadingStatus MessageItem::threadingStatus() const
478{
479 Q_D(const MessageItem);
480 return d->mThreadingStatus;
481}
482
483void MessageItem::setThreadingStatus(ThreadingStatus threadingStatus)
484{
486 d->mThreadingStatus = threadingStatus;
487}
488
489unsigned long MessageItem::uniqueId() const
490{
491 Q_D(const MessageItem);
492 return d->mAkonadiItem.id();
493}
494
495Akonadi::Item MessageList::Core::MessageItem::akonadiItem() const
496{
497 Q_D(const MessageItem);
498 return d->mAkonadiItem;
499}
500
501void MessageList::Core::MessageItem::setAkonadiItem(const Akonadi::Item &item)
502{
504 d->mAkonadiItem = item;
505}
506
507MessageItem *MessageItem::topmostMessage()
508{
509 if (!parent()) {
510 return this;
511 }
512 if (parent()->type() == Item::Message) {
513 return static_cast<MessageItem *>(parent())->topmostMessage();
514 }
515 return this;
516}
517
518QString MessageItem::accessibleTextForField(Theme::ContentItem::Type field)
519{
520 switch (field) {
522 return d_ptr->mSubject;
524 return d_ptr->mSender;
526 return d_ptr->mReceiver;
528 return senderOrReceiver();
530 return formattedDate();
532 return formattedSize();
534 return status().isReplied() ? i18nc("Status of an item", "Replied") : QString();
536 return status().isRead() ? i18nc("Status of an item", "Read") : i18nc("Status of an item", "Unread");
538 return accessibleTextForField(Theme::ContentItem::ReadStateIcon) + accessibleTextForField(Theme::ContentItem::RepliedStateIcon);
539 default:
540 return {};
541 }
542}
543
544QString MessageItem::accessibleText(const Theme *theme, int columnIndex)
545{
546 QStringList rowsTexts;
547 const QList<Theme::Row *> rows = theme->column(columnIndex)->messageRows();
548 rowsTexts.reserve(rows.count());
549
550 for (Theme::Row *row : rows) {
551 QStringList leftStrings;
552 QStringList rightStrings;
553 const auto leftItems = row->leftItems();
554 leftStrings.reserve(leftItems.count());
555 for (Theme::ContentItem *contentItem : std::as_const(leftItems)) {
556 leftStrings.append(accessibleTextForField(contentItem->type()));
557 }
558
559 const auto rightItems = row->rightItems();
560 rightStrings.reserve(rightItems.count());
561 for (Theme::ContentItem *contentItem : rightItems) {
562 rightStrings.insert(rightStrings.begin(), accessibleTextForField(contentItem->type()));
563 }
564
565 rowsTexts.append((leftStrings + rightStrings).join(QLatin1Char(' ')));
566 }
567
568 return rowsTexts.join(QLatin1Char(' '));
569}
570
572{
573 list.append(this);
574 const auto childList = childItems();
575 if (!childList) {
576 return;
577 }
578 for (const auto child : std::as_const(*childList)) {
579 Q_ASSERT(child->type() == Item::Message);
580 static_cast<MessageItem *>(child)->subTreeToList(list);
581 }
582}
583
584void MessageItem::setUnreadMessageColor(const QColor &color)
585{
586 s_settings->mColorUnreadMessage = color;
587}
588
589void MessageItem::setImportantMessageColor(const QColor &color)
590{
591 s_settings->mColorImportantMessage = color;
592}
593
594void MessageItem::setToDoMessageColor(const QColor &color)
595{
596 s_settings->mColorToDoMessage = color;
597}
598
599void MessageItem::setGeneralFont(const QFont &font)
600{
601 s_settings->mFont = font;
602}
603
604void MessageItem::setUnreadMessageFont(const QFont &font)
605{
606 s_settings->mFontUnreadMessage = font;
607}
608
609void MessageItem::setImportantMessageFont(const QFont &font)
610{
611 s_settings->mFontImportantMessage = font;
612}
613
614void MessageItem::setToDoMessageFont(const QFont &font)
615{
616 s_settings->mFontToDoMessage = font;
617}
618
619FakeItemPrivate::FakeItemPrivate(FakeItem *qq)
620 : MessageItemPrivate(qq)
621{
622}
623
624FakeItem::FakeItem()
625 : MessageItem(new FakeItemPrivate(this))
626{
627}
628
629FakeItem::~FakeItem()
630{
631 Q_D(const FakeItem);
632 qDeleteAll(d->mFakeTags);
633}
634
636{
637 Q_D(const FakeItem);
638 return d->mFakeTags;
639}
640
642{
643 Q_D(FakeItem);
644 d->mFakeTags = tagList;
645}
646
648{
649 return true;
650}
651
652TagCache::TagCache()
653 : QObject()
654 , mMonitor(new Akonadi::Monitor(this))
655{
656 mCache.setMaxCost(100);
657 mMonitor->setObjectName(QLatin1StringView("MessageListTagCacheMonitor"));
658 mMonitor->setTypeMonitored(Akonadi::Monitor::Tags);
659 mMonitor->tagFetchScope().fetchAttribute<Akonadi::TagAttribute>();
660 connect(mMonitor, &Akonadi::Monitor::tagAdded, this, &TagCache::onTagAdded);
661 connect(mMonitor, &Akonadi::Monitor::tagRemoved, this, &TagCache::onTagRemoved);
662 connect(mMonitor, &Akonadi::Monitor::tagChanged, this, &TagCache::onTagChanged);
663}
664
665void TagCache::onTagAdded(const Akonadi::Tag &tag)
666{
667 mCache.insert(tag.id(), new Akonadi::Tag(tag));
668}
669
670void TagCache::onTagChanged(const Akonadi::Tag &tag)
671{
672 mCache.remove(tag.id());
673}
674
675void TagCache::onTagRemoved(const Akonadi::Tag &tag)
676{
677 mCache.remove(tag.id());
678}
679
680void TagCache::retrieveTags(const Akonadi::Tag::List &tags, MessageItemPrivate *m)
681{
682 // Retrieval is in progress
683 if (mRequests.key(m)) {
684 return;
685 }
686 Akonadi::Tag::List toFetch;
687 Akonadi::Tag::List available;
688 for (const Akonadi::Tag &tag : tags) {
689 if (mCache.contains(tag.id())) {
690 available << *mCache.object(tag.id());
691 } else {
692 toFetch << tag;
693 }
694 }
695 // Because fillTagList expects to be called once we either fetch all or none
696 if (!toFetch.isEmpty()) {
697 auto tagFetchJob = new Akonadi::TagFetchJob(tags, this);
698 tagFetchJob->fetchScope().fetchAttribute<Akonadi::TagAttribute>();
699 connect(tagFetchJob, &Akonadi::TagFetchJob::result, this, &TagCache::onTagsFetched);
700 mRequests.insert(tagFetchJob, m);
701 } else {
702 m->fillTagList(available);
703 }
704}
705
706void TagCache::cancelRequest(MessageItemPrivate *m)
707{
708 const QList<KJob *> keys = mRequests.keys(m);
709 for (KJob *job : keys) {
710 mRequests.remove(job);
711 }
712}
713
714void TagCache::onTagsFetched(KJob *job)
715{
716 if (job->error()) {
717 qCWarning(MESSAGELIST_LOG) << "Failed to fetch tags: " << job->errorString();
718 return;
719 }
720 auto fetchJob = static_cast<Akonadi::TagFetchJob *>(job);
721 const auto tags{fetchJob->tags()};
722 for (const Akonadi::Tag &tag : tags) {
723 mCache.insert(tag.id(), new Akonadi::Tag(tag));
724 }
725 if (auto m = mRequests.take(fetchJob)) {
726 m->fillTagList(fetchJob->tags());
727 }
728}
729
730#include "moc_messageitem_p.cpp"
bool isImportant() const
bool isReplied() const
void tagRemoved(const Akonadi::Tag &tag)
void tagChanged(const Akonadi::Tag &tag)
void tagAdded(const Akonadi::Tag &tag)
Tag::List tags() const
Id id() const
virtual QString errorString() const
int error() const
void result(KJob *job)
A message item that can have a fake tag list and a fake annotation.
void setFakeTags(const QList< Tag * > &tagList)
Sets a list of fake tags for this item.
QList< Tag * > tagList() const override
Reimplemented to return the fake tag list.
bool hasAnnotation() const override
Reimplemented to always return true.
const QString & senderOrReceiver() const
Returns the sender or the receiver, depending on the underlying StorageModel settings.
Definition item.cpp:517
QString formattedDate() const
A string with a text rappresentation of date() obtained via Manager.
Definition item.cpp:306
const Akonadi::MessageStatus & status() const
Returns the status associated to this Item.
Definition item.cpp:447
Type type() const
Returns the type of this item.
Definition item.cpp:343
Item * parent() const
Returns the parent Item in the tree, or 0 if this item isn't attached to the tree.
Definition item.cpp:437
@ Message
This item is a MessageItem.
Definition item.h:46
QString formattedSize() const
A string with a text rappresentation of size().
Definition item.cpp:300
QList< Item * > * childItems() const
Return the list of child items.
Definition item.cpp:59
The MessageItem class.
Definition messageitem.h:35
const Tag * findTag(const QString &szTagId) const
Returns Tag associated to this message that has the specified id or 0 if no such tag exists.
void editAnnotation(QWidget *parent)
Shows a dialog to edit or delete the annotation.
void invalidateTagCache()
Deletes all cached tags.
@ ParentMissing
this message might belong to a thread but its parent is actually missing
Definition messageitem.h:63
virtual bool hasAnnotation() const
Returns true if this message has an annotation.
void subTreeToList(QList< MessageItem * > &list)
Appends the whole subtree originating at this item to the specified list.
virtual QList< Tag * > tagList() const
Returns the list of tags for this item.
void invalidateAnnotationCache()
Same as invalidateTagCache(), only for the annotation.
QString annotation() const
Returns the annotation of the message, given that hasAnnotation() is true.
An invariant index that can be ALWAYS used to reference an item inside a QAbstractItemModel.
const QList< Row * > & messageRows() const
Returns the list of rows visible in this column for a MessageItem.
Definition theme.cpp:718
The ContentItem class defines a content item inside a Row.
Definition theme.h:56
Type
The available ContentItem types.
Definition theme.h:106
@ CombinedReadRepliedStateIcon
The combined icon that displays the unread/read/replied/forwarded state (never disabled)
Definition theme.h:190
@ Date
Formatted date time of the message/group.
Definition theme.h:114
@ ReadStateIcon
The icon that displays the unread/read state (never disabled)
Definition theme.h:134
@ RepliedStateIcon
The icon that displays the replied/forwarded state (may be disabled)
Definition theme.h:142
@ SenderOrReceiver
From: or To: strip, depending on the folder settings.
Definition theme.h:118
@ Subject
Display the subject of the message item.
Definition theme.h:110
@ Size
Formatted size of the message.
Definition theme.h:130
@ Receiver
To: strip, always.
Definition theme.h:126
@ Sender
From: strip, always.
Definition theme.h:122
The Row class defines a row of items inside a Column.
Definition theme.h:413
The Theme class defines the visual appearance of the MessageList.
Definition theme.h:48
Column * column(int idx) const
Returns a pointer to the column at the specified index or 0 if there is no such column.
Definition theme.cpp:973
QString i18nc(const char *context, const char *text, const TYPE &arg...)
The implementation independent part of the MessageList library.
Definition aggregation.h:22
bool isEmpty() const const
bool fromString(const QString &descrip)
QIcon fromTheme(const QString &name)
QString name() const const
void append(QList< T > &&value)
iterator begin()
qsizetype count() const const
iterator insert(const_iterator before, parameter_type value)
void remove(qsizetype i, qsizetype n)
void reserve(qsizetype size)
T qobject_cast(QObject *object)
void setObjectName(QAnyStringView name)
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString join(QChar separator) const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.