Messagelib

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

KDE's Doxygen guidelines are available online.