KCalendarCore

icalformat.cpp
Go to the documentation of this file.
1/*
2 This file is part of the kcalcore library.
3
4 SPDX-FileCopyrightText: 2001 Cornelius Schumacher <schumacher@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8/**
9 @file
10 This file is part of the API for handling calendar data and
11 defines the ICalFormat class.
12
13 @brief
14 iCalendar format implementation: a layer of abstraction for libical.
15
16 @author Cornelius Schumacher <schumacher@kde.org>
17*/
18#include "icalformat.h"
19#include "calendar_p.h"
20#include "calformat_p.h"
21#include "icalformat_p.h"
22#include "icaltimezones_p.h"
23#include "kcalendarcore_debug.h"
24#include "memorycalendar.h"
25
26#include <QFile>
27#include <QSaveFile>
28#include <QTimeZone>
29
30extern "C" {
31#include <libical/ical.h>
32#include <libical/icalmemory.h>
33#include <libical/icalparser.h>
34#include <libical/icalrestriction.h>
35#include <libical/icalss.h>
36}
37
38using namespace KCalendarCore;
39
40//@cond PRIVATE
41class KCalendarCore::ICalFormatPrivate : public KCalendarCore::CalFormatPrivate
42{
43public:
44 ICalFormatPrivate(ICalFormat *parent)
45 : mImpl(parent)
46 , mTimeZone(QTimeZone::utc())
47 {
48 }
49 ICalFormatImpl mImpl;
50 QTimeZone mTimeZone;
51};
52//@endcond
53
55 : CalFormat(new ICalFormatPrivate(this))
56{
57}
58
60{
61 icalmemory_free_ring();
62}
63
64bool ICalFormat::load(const Calendar::Ptr &calendar, const QString &fileName)
65{
66 qCDebug(KCALCORE_LOG) << fileName;
67
69
70 QFile file(fileName);
71 if (!file.open(QIODevice::ReadOnly)) {
72 qCritical() << "load error: unable to open " << fileName;
74 return false;
75 }
76 const QByteArray text = file.readAll().trimmed();
77 file.close();
78
79 if (!text.isEmpty()) {
80 if (!fromRawString(calendar, text)) {
81 qCWarning(KCALCORE_LOG) << fileName << " is not a valid iCalendar file";
83 return false;
84 }
85 }
86
87 // Note: we consider empty files to be valid
88
89 return true;
90}
91
92bool ICalFormat::save(const Calendar::Ptr &calendar, const QString &fileName)
93{
94 qCDebug(KCALCORE_LOG) << fileName;
95
97
98 QString text = toString(calendar);
99 if (text.isEmpty()) {
100 return false;
101 }
102
103 // Write backup file
104 const QString backupFile = fileName + QLatin1Char('~');
105 QFile::remove(backupFile);
106 QFile::copy(fileName, backupFile);
107
108 QSaveFile file(fileName);
109 if (!file.open(QIODevice::WriteOnly)) {
110 qCritical() << "file open error: " << file.errorString() << ";filename=" << fileName;
111 setException(new Exception(Exception::SaveErrorOpenFile, QStringList(fileName)));
112
113 return false;
114 }
115
116 // Convert to UTF8 and save
117 QByteArray textUtf8 = text.toUtf8();
118 file.write(textUtf8.data(), textUtf8.size());
119 // QSaveFile doesn't report a write error when the device is full (see Qt
120 // bug 75077), so check that the data can actually be written.
121 if (!file.flush()) {
122 qCDebug(KCALCORE_LOG) << "file write error (flush failed)";
123 setException(new Exception(Exception::SaveErrorSaveFile, QStringList(fileName)));
124 return false;
125 }
126
127 if (!file.commit()) {
128 qCDebug(KCALCORE_LOG) << "file finalize error:" << file.errorString();
129 setException(new Exception(Exception::SaveErrorSaveFile, QStringList(fileName)));
130
131 return false;
132 }
133
134 return true;
135}
136
138{
140
141 // Let's defend const correctness until the very gates of hell^Wlibical
142 icalcomponent *calendar = icalcomponent_new_from_string(const_cast<char *>(string.constData()));
143 if (!calendar) {
144 qCritical() << "parse error from icalcomponent_new_from_string. string=" << QString::fromLatin1(string);
146 return Incidence::Ptr();
147 }
148
149 ICalTimeZoneCache tzCache;
150 ICalTimeZoneParser parser(&tzCache);
151 parser.parse(calendar);
152
153 Incidence::Ptr incidence;
154 if (icalcomponent_isa(calendar) == ICAL_VCALENDAR_COMPONENT) {
155 incidence = d->mImpl.readOneIncidence(calendar, &tzCache);
156 } else if (icalcomponent_isa(calendar) == ICAL_XROOT_COMPONENT) {
157 icalcomponent *comp = icalcomponent_get_first_component(calendar, ICAL_VCALENDAR_COMPONENT);
158 if (comp) {
159 incidence = d->mImpl.readOneIncidence(comp, &tzCache);
160 }
161 }
162
163 if (!incidence) {
164 qCDebug(KCALCORE_LOG) << "No VCALENDAR component found";
166 }
167
168 icalcomponent_free(calendar);
169 icalmemory_free_ring();
170
171 return incidence;
172}
173
175{
177
178 // Get first VCALENDAR component.
179 // TODO: Handle more than one VCALENDAR or non-VCALENDAR top components
180 icalcomponent *calendar;
181
182 // Let's defend const correctness until the very gates of hell^Wlibical
183 calendar = icalcomponent_new_from_string(const_cast<char *>(string.constData()));
184 if (!calendar) {
185 qCritical() << "parse error from icalcomponent_new_from_string. string=" << QString::fromLatin1(string);
187 return false;
188 }
189
190 bool success = true;
191
192 if (icalcomponent_isa(calendar) == ICAL_XROOT_COMPONENT) {
193 icalcomponent *comp;
194 for (comp = icalcomponent_get_first_component(calendar, ICAL_VCALENDAR_COMPONENT); comp;
195 comp = icalcomponent_get_next_component(calendar, ICAL_VCALENDAR_COMPONENT)) {
196 // put all objects into their proper places
197 if (!d->mImpl.populate(cal, comp)) {
198 qCritical() << "Could not populate calendar";
199 if (!exception()) {
201 }
202 success = false;
203 } else {
204 setLoadedProductId(d->mImpl.loadedProductId());
205 }
206 }
207 } else if (icalcomponent_isa(calendar) != ICAL_VCALENDAR_COMPONENT) {
208 qCDebug(KCALCORE_LOG) << "No VCALENDAR component found";
210 success = false;
211 } else {
212 // put all objects into their proper places
213 if (!d->mImpl.populate(cal, calendar)) {
214 qCDebug(KCALCORE_LOG) << "Could not populate calendar";
215 if (!exception()) {
217 }
218 success = false;
219 } else {
220 setLoadedProductId(d->mImpl.loadedProductId());
221 }
222 }
223
224 icalcomponent_free(calendar);
225 icalmemory_free_ring();
226
227 return success;
228}
229
231{
233
234 MemoryCalendar::Ptr cal(new MemoryCalendar(d->mTimeZone));
235 fromString(cal, string);
236
237 const Incidence::List list = cal->incidences();
238 return !list.isEmpty() ? list.first() : Incidence::Ptr();
239}
240
242{
244
245 icalcomponent *calendar = d->mImpl.createCalendarComponent(cal);
246 icalcomponent *component;
247
248 QList<QTimeZone> tzUsedList;
249 TimeZoneEarliestDate earliestTz;
250
251 // todos
252 Todo::List todoList = cal->rawTodos();
253 for (auto it = todoList.cbegin(), end = todoList.cend(); it != end; ++it) {
254 component = d->mImpl.writeTodo(*it, &tzUsedList);
255 icalcomponent_add_component(calendar, component);
256 ICalTimeZoneParser::updateTzEarliestDate((*it), &earliestTz);
257 }
258 // events
259 Event::List events = cal->rawEvents();
260 for (auto it = events.cbegin(), end = events.cend(); it != end; ++it) {
261 component = d->mImpl.writeEvent(*it, &tzUsedList);
262 icalcomponent_add_component(calendar, component);
263 ICalTimeZoneParser::updateTzEarliestDate((*it), &earliestTz);
264 }
265
266 // journals
267 Journal::List journals = cal->rawJournals();
268 for (auto it = journals.cbegin(), end = journals.cend(); it != end; ++it) {
269 component = d->mImpl.writeJournal(*it, &tzUsedList);
270 icalcomponent_add_component(calendar, component);
271 ICalTimeZoneParser::updateTzEarliestDate((*it), &earliestTz);
272 }
273
274 // time zones
275 if (todoList.isEmpty() && events.isEmpty() && journals.isEmpty()) {
276 // no incidences means no used timezones, use all timezones
277 // this will export a calendar having only timezone definitions
278 tzUsedList = cal->d->mTimeZones;
279 }
280 for (const auto &qtz : std::as_const(tzUsedList)) {
281 if (qtz != QTimeZone::utc()) {
282 icaltimezone *tz = ICalTimeZoneParser::icaltimezoneFromQTimeZone(qtz, earliestTz[qtz]);
283 if (!tz) {
284 qCritical() << "bad time zone";
285 } else {
286 component = icalcomponent_new_clone(icaltimezone_get_component(tz));
287 icalcomponent_add_component(calendar, component);
288 icaltimezone_free(tz, 1);
289 }
290 }
291 }
292
293 char *const componentString = icalcomponent_as_ical_string_r(calendar);
294 const QString &text = QString::fromUtf8(componentString);
295 free(componentString);
296
297 icalcomponent_free(calendar);
298 icalmemory_free_ring();
299
300 if (text.isEmpty()) {
301 setException(new Exception(Exception::LibICalError));
302 }
303
304 return text;
305}
306
308{
310
311 MemoryCalendar::Ptr cal(new MemoryCalendar(d->mTimeZone));
312 cal->addIncidence(Incidence::Ptr(incidence->clone()));
313 return toString(cal.staticCast<Calendar>());
314}
315
317{
318 return QString::fromUtf8(toRawString(incidence));
319}
320
322{
324 TimeZoneList tzUsedList;
325
326 icalcomponent *component = d->mImpl.writeIncidence(incidence, iTIPRequest, &tzUsedList);
327
328 QByteArray text = icalcomponent_as_ical_string(component);
329
330 TimeZoneEarliestDate earliestTzDt;
331 ICalTimeZoneParser::updateTzEarliestDate(incidence, &earliestTzDt);
332
333 // time zones
334 for (const auto &qtz : std::as_const(tzUsedList)) {
335 if (qtz != QTimeZone::utc()) {
336 icaltimezone *tz = ICalTimeZoneParser::icaltimezoneFromQTimeZone(qtz, earliestTzDt[qtz]);
337 if (!tz) {
338 qCritical() << "bad time zone";
339 } else {
340 icalcomponent *tzcomponent = icaltimezone_get_component(tz);
341 icalcomponent_add_component(component, component);
342 text.append(icalcomponent_as_ical_string(tzcomponent));
343 icaltimezone_free(tz, 1);
344 }
345 }
346 }
347
348 icalcomponent_free(component);
349
350 return text;
351}
352
354{
356 icalproperty *property = icalproperty_new_rrule(d->mImpl.writeRecurrenceRule(recurrence));
357 QString text = QString::fromUtf8(icalproperty_as_ical_string(property));
358 icalproperty_free(property);
359 return text;
360}
361
363{
364 Q_D(const ICalFormat);
365 const auto icalDuration = d->mImpl.writeICalDuration(duration);
366 // contrary to the libical API docs, the returned string is actually freed by icalmemory_free_ring,
367 // freeing it here explicitly causes a double deletion failure
368 return QString::fromUtf8(icaldurationtype_as_ical_string(icalDuration));
369}
370
371bool ICalFormat::fromString(RecurrenceRule *recurrence, const QString &rrule)
372{
373 if (!recurrence) {
374 return false;
375 }
376 bool success = true;
377 icalerror_clear_errno();
378 struct icalrecurrencetype recur = icalrecurrencetype_from_string(rrule.toLatin1().constData());
379 if (icalerrno != ICAL_NO_ERROR) {
380 qCDebug(KCALCORE_LOG) << "Recurrence parsing error:" << icalerror_strerror(icalerrno);
381 success = false;
382 }
383
384 if (success) {
385 ICalFormatImpl::readRecurrence(recur, recurrence);
386 }
387
388 return success;
389}
390
392{
393 icalerror_clear_errno();
394 const auto icalDuration = icaldurationtype_from_string(duration.toUtf8().constData());
395 if (icalerrno != ICAL_NO_ERROR) {
396 qCDebug(KCALCORE_LOG) << "Duration parsing error:" << icalerror_strerror(icalerrno);
397 return {};
398 }
399 return ICalFormatImpl::readICalDuration(icalDuration);
400}
401
403{
405 icalcomponent *message = nullptr;
406
407 if (incidence->type() == Incidence::TypeEvent || incidence->type() == Incidence::TypeTodo) {
408 Incidence::Ptr i = incidence.staticCast<Incidence>();
409
410 // Recurring events need timezone information to allow proper calculations
411 // across timezones with different DST.
412 const bool useUtcTimes = !i->recurs() && !i->allDay();
413
414 const bool hasSchedulingId = (i->schedulingID() != i->uid());
415
416 const bool incidenceNeedChanges = (useUtcTimes || hasSchedulingId);
417
418 if (incidenceNeedChanges) {
419 // The incidence need changes, so clone it before we continue
420 i = Incidence::Ptr(i->clone());
421
422 // Handle conversion to UTC times
423 if (useUtcTimes) {
424 i->shiftTimes(QTimeZone::utc(), QTimeZone::utc());
425 }
426
427 // Handle scheduling ID being present
428 if (hasSchedulingId) {
429 // We have a separation of scheduling ID and UID
430 i->setSchedulingID(QString(), i->schedulingID());
431 }
432
433 // Build the message with the cloned incidence
434 message = d->mImpl.createScheduleComponent(i, method);
435 }
436 }
437
438 if (message == nullptr) {
439 message = d->mImpl.createScheduleComponent(incidence, method);
440 }
441
442 QString messageText = QString::fromUtf8(icalcomponent_as_ical_string(message));
443
444 icalcomponent_free(message);
445 return messageText;
446}
447
449{
452
453 icalcomponent *message = icalparser_parse_string(str.toUtf8().constData());
454
455 if (!message) {
456 return FreeBusy::Ptr();
457 }
458
459 FreeBusy::Ptr freeBusy;
460
461 icalcomponent *c = nullptr;
462 for (c = icalcomponent_get_first_component(message, ICAL_VFREEBUSY_COMPONENT); c != nullptr;
463 c = icalcomponent_get_next_component(message, ICAL_VFREEBUSY_COMPONENT)) {
464 FreeBusy::Ptr fb = d->mImpl.readFreeBusy(c);
465
466 if (freeBusy) {
467 freeBusy->merge(fb);
468 } else {
469 freeBusy = fb;
470 }
471 }
472
473 if (!freeBusy) {
474 qCDebug(KCALCORE_LOG) << "object is not a freebusy.";
475 }
476
477 icalcomponent_free(message);
478
479 return freeBusy;
480}
481
483{
485 setTimeZone(cal->timeZone());
487
488 if (messageText.isEmpty()) {
489 setException(new Exception(Exception::ParseErrorEmptyMessage));
490 return ScheduleMessage::Ptr();
491 }
492
493 icalcomponent *message = icalparser_parse_string(messageText.toUtf8().constData());
494
495 if (!message) {
496 setException(new Exception(Exception::ParseErrorUnableToParse));
497
498 return ScheduleMessage::Ptr();
499 }
500
501 icalproperty *m = icalcomponent_get_first_property(message, ICAL_METHOD_PROPERTY);
502 if (!m) {
503 setException(new Exception(Exception::ParseErrorMethodProperty));
504
505 return ScheduleMessage::Ptr();
506 }
507
508 // Populate the message's time zone collection with all VTIMEZONE components
509 ICalTimeZoneCache tzlist;
510 ICalTimeZoneParser parser(&tzlist);
511 parser.parse(message);
512
513 IncidenceBase::Ptr incidence;
514 icalcomponent *c = icalcomponent_get_first_component(message, ICAL_VEVENT_COMPONENT);
515 if (c) {
516 incidence = d->mImpl.readEvent(c, &tzlist).staticCast<IncidenceBase>();
517 }
518
519 if (!incidence) {
520 c = icalcomponent_get_first_component(message, ICAL_VTODO_COMPONENT);
521 if (c) {
522 incidence = d->mImpl.readTodo(c, &tzlist).staticCast<IncidenceBase>();
523 }
524 }
525
526 if (!incidence) {
527 c = icalcomponent_get_first_component(message, ICAL_VJOURNAL_COMPONENT);
528 if (c) {
529 incidence = d->mImpl.readJournal(c, &tzlist).staticCast<IncidenceBase>();
530 }
531 }
532
533 if (!incidence) {
534 c = icalcomponent_get_first_component(message, ICAL_VFREEBUSY_COMPONENT);
535 if (c) {
536 incidence = d->mImpl.readFreeBusy(c).staticCast<IncidenceBase>();
537 }
538 }
539
540 if (!incidence) {
541 qCDebug(KCALCORE_LOG) << "object is not a freebusy, event, todo or journal";
542 setException(new Exception(Exception::ParseErrorNotIncidence));
543
544 return ScheduleMessage::Ptr();
545 }
546
547 icalproperty_method icalmethod = icalproperty_get_method(m);
548 iTIPMethod method = ICalFormatImpl::fromIcalEnum(icalmethod);
549
550 if (!icalrestriction_check(message)) {
551 qCWarning(KCALCORE_LOG) << "\nkcalcore library reported a problem while parsing:";
552 qCWarning(KCALCORE_LOG) << ScheduleMessage::methodName(method) << ":" << d->mImpl.extractErrorProperty(c);
553 }
554
555 Incidence::Ptr existingIncidence = cal->incidence(incidence->uid());
556
557 icalcomponent *calendarComponent = nullptr;
558 if (existingIncidence) {
559 calendarComponent = d->mImpl.createCalendarComponent(cal);
560
561 // TODO: check, if cast is required, or if it can be done by virtual funcs.
562 // TODO: Use a visitor for this!
563 if (existingIncidence->type() == Incidence::TypeTodo) {
564 Todo::Ptr todo = existingIncidence.staticCast<Todo>();
565 icalcomponent_add_component(calendarComponent, d->mImpl.writeTodo(todo));
566 }
567 if (existingIncidence->type() == Incidence::TypeEvent) {
568 Event::Ptr event = existingIncidence.staticCast<Event>();
569 icalcomponent_add_component(calendarComponent, d->mImpl.writeEvent(event));
570 }
571 } else {
572 icalcomponent_free(message);
573 return ScheduleMessage::Ptr(new ScheduleMessage(incidence, method, ScheduleMessage::Unknown));
574 }
575
576 icalproperty_xlicclass result = icalclassify(message, calendarComponent, static_cast<const char *>(""));
577
579
580 switch (result) {
581 case ICAL_XLICCLASS_PUBLISHNEW:
583 break;
584 case ICAL_XLICCLASS_PUBLISHUPDATE:
586 break;
587 case ICAL_XLICCLASS_OBSOLETE:
589 break;
590 case ICAL_XLICCLASS_REQUESTNEW:
592 break;
593 case ICAL_XLICCLASS_REQUESTUPDATE:
595 break;
596 case ICAL_XLICCLASS_UNKNOWN:
597 default:
599 break;
600 }
601
602 icalcomponent_free(message);
603 icalcomponent_free(calendarComponent);
604
605 return ScheduleMessage::Ptr(new ScheduleMessage(incidence, method, status));
606}
607
609{
611 d->mTimeZone = timeZone;
612}
613
615{
616 Q_D(const ICalFormat);
617 return d->mTimeZone;
618}
619
621{
622 Q_D(const ICalFormat);
623 return d->mTimeZone.id();
624}
An abstract base class that provides an interface to various calendar formats.
Definition calformat.h:39
void setLoadedProductId(const QString &id)
Sets the PRODID string loaded from calendar file.
Definition calformat.cpp:83
void clearException()
Clears the exception status.
Definition calformat.cpp:47
Exception * exception() const
Returns an exception, if there is any, containing information about the last error that occurred.
Definition calformat.cpp:57
void setException(Exception *error)
Sets an exception that is to be used by the functions of this class to report errors.
Definition calformat.cpp:52
Represents the main calendar class.
Definition calendar.h:133
Represents a span of time measured in seconds or days.
Definition duration.h:44
This class provides an Event in the sense of RFC2445.
Definition event.h:33
Exception base class, currently used as a fancy kind of error code and not as an C++ exception.
Definition exceptions.h:42
@ ParseErrorIcal
Parse error in libical.
Definition exceptions.h:50
@ NoCalendar
No calendar component found.
Definition exceptions.h:52
@ ParseErrorKcal
Parse error in libkcal.
Definition exceptions.h:51
QSharedPointer< FreeBusy > Ptr
A shared pointer to a FreeBusy object.
Definition freebusy.h:51
iCalendar format implementation.
Definition icalformat.h:45
Incidence::Ptr readIncidence(const QByteArray &string)
Parses a bytearray, returning the first iCal component as an Incidence, ignoring timezone information...
FreeBusy::Ptr parseFreeBusy(const QString &string)
Converts a QString into a FreeBusy object.
QString toString(const Calendar::Ptr &calendar) override
QString createScheduleMessage(const IncidenceBase::Ptr &incidence, iTIPMethod method)
Creates a scheduling message string for an Incidence.
bool save(const Calendar::Ptr &calendar, const QString &fileName) override
Duration durationFromString(const QString &duration) const
Parses a string representation of a duration.
QTimeZone timeZone() const
Returns the iCalendar time zone.
QString toICalString(const Incidence::Ptr &incidence)
Converts an Incidence to iCalendar formatted text.
QByteArray toRawString(const Incidence::Ptr &incidence)
Converts an Incidence to a QByteArray.
bool load(const Calendar::Ptr &calendar, const QString &fileName) override
void setTimeZone(const QTimeZone &timeZone)
Sets the iCalendar time zone.
ScheduleMessage::Ptr parseScheduleMessage(const Calendar::Ptr &calendar, const QString &string)
Parses a Calendar scheduling message string into ScheduleMessage object.
ICalFormat()
Constructor a new iCalendar Format object.
~ICalFormat() override
Destructor.
Incidence::Ptr fromString(const QString &string)
Parses a string, returning the first iCal component as an Incidence.
QByteArray timeZoneId() const
Returns the timezone id string used by the iCalendar; an empty string if the iCalendar does not have ...
bool fromRawString(const Calendar::Ptr &calendar, const QByteArray &string) override
An abstract class that provides a common base for all calendar incidence classes.
@ TypeEvent
Type is an event.
Provides the abstract base class common to non-FreeBusy (Events, To-dos, Journals) calendar component...
Definition incidence.h:60
QSharedPointer< Incidence > Ptr
A shared pointer to an Incidence.
Definition incidence.h:117
This class provides a calendar stored in memory.
This class represents a recurrence rule for a calendar incidence.
A Scheduling message class.
@ RequestUpdate
Request updated message.
@ RequestNew
Request new message posting.
@ PublishNew
New message posting.
QSharedPointer< ScheduleMessage > Ptr
A shared pointer to a ScheduleMessage.
static QString methodName(iTIPMethod method)
Returns a machine-readable (not translatable) name for a iTIP method.
Provides a To-do in the sense of RFC2445.
Definition todo.h:34
Q_SCRIPTABLE CaptureState status()
This file is part of the API for handling calendar data and defines the ICalFormat class.
This file is part of the API for handling calendar data and defines the MemoryCalendar class.
Namespace for all KCalendarCore types.
Definition alarm.h:37
iTIPMethod
iTIP methods.
@ iTIPRequest
Event, to-do or freebusy scheduling request.
QByteArray & append(QByteArrayView data)
const char * constData() const const
char * data()
bool isEmpty() const const
qsizetype size() const const
QByteArray trimmed() const const
bool copy(const QString &fileName, const QString &newName)
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
bool remove()
virtual void close() override
bool flush()
QString errorString() const const
QByteArray readAll()
qint64 write(const QByteArray &data)
const_iterator cbegin() const const
const_iterator cend() const const
T & first()
bool isEmpty() const const
bool commit()
virtual bool open(OpenMode mode) override
QSharedPointer< X > staticCast() const const
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QByteArray toLatin1() const const
QByteArray toUtf8() const const
QTimeZone utc()
Q_D(Todo)
Private class that helps to provide binary compatibility between releases.
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:08:40 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.