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

KDE's Doxygen guidelines are available online.