KTextEditor

katetemplatehandler.cpp
1/*
2 SPDX-FileCopyrightText: 2004, 2010 Joseph Wenninger <jowenn@kde.org>
3 SPDX-FileCopyrightText: 2009 Milian Wolff <mail@milianw.de>
4 SPDX-FileCopyrightText: 2014 Sven Brauch <svenbrauch@gmail.com>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include <QKeyEvent>
10#include <QQueue>
11#include <QRegularExpression>
12
13#include <ktexteditor/movingcursor.h>
14#include <ktexteditor/movingrange.h>
15
16#include "kateconfig.h"
17#include "katedocument.h"
18#include "kateglobal.h"
19#include "katepartdebug.h"
20#include "kateregexpsearch.h"
21#include "katetemplatehandler.h"
22#include "kateundomanager.h"
23#include "kateview.h"
24#include "script/katescriptmanager.h"
25
26using namespace KTextEditor;
27
28#define ifDebug(x)
29
30KateTemplateHandler::KateTemplateHandler(KTextEditor::ViewPrivate *view,
31 Cursor position,
32 const QString &templateString,
33 const QString &script,
34 KateUndoManager *undoManager)
35 : QObject(view)
36 , m_view(view)
37 , m_undoManager(undoManager)
38 , m_wholeTemplateRange()
39 , m_internalEdit(false)
40 , m_templateScript(script, KateScript::InputSCRIPT)
41{
42 Q_ASSERT(m_view);
43
44 m_templateScript.setView(m_view);
45
46 // remember selection, it will be lost when inserting the template
47 std::unique_ptr<MovingRange> selection(doc()->newMovingRange(m_view->selectionRange(), MovingRange::DoNotExpand));
48
49 m_undoManager->setAllowComplexMerge(true);
50
51 {
52 connect(doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &KateTemplateHandler::slotTemplateInserted);
54 // insert the raw template string
55 if (!doc()->insertText(position, templateString)) {
57 return;
58 }
59 // now there must be a range, caught by the textInserted slot
60 Q_ASSERT(m_wholeTemplateRange);
61 doc()->align(m_view, *m_wholeTemplateRange);
62 }
63
64 // before initialization, restore selection (if any) so user scripts can retrieve it
65 m_view->setSelection(selection->toRange());
66 initializeTemplate();
67 // then delete the selected text (if any); it was replaced by the template
68 doc()->removeText(selection->toRange());
69
70 const bool have_editable_field = std::any_of(m_fields.constBegin(), m_fields.constEnd(), [](const TemplateField &field) {
71 return (field.kind == TemplateField::Editable);
72 });
73 // only do complex stuff when required
74 if (have_editable_field) {
75 const auto views = doc()->views();
76 for (View *view : views) {
77 setupEventHandler(view);
78 }
79
80 // place the cursor at the first field and select stuff
81 jump(1, true);
82
83 connect(doc(), &KTextEditor::Document::viewCreated, this, &KateTemplateHandler::slotViewCreated);
85 updateDependentFields(doc, range, false);
86 });
88 updateDependentFields(doc, range, true);
89 });
91
92 } else {
93 // when no interesting ranges got added, we can terminate directly
94 jumpToFinalCursorPosition();
96 }
97}
98
99KateTemplateHandler::~KateTemplateHandler()
100{
101 m_undoManager->setAllowComplexMerge(false);
102}
103
104void KateTemplateHandler::sortFields()
105{
106 std::sort(m_fields.begin(), m_fields.end(), [](const TemplateField &l, const TemplateField &r) {
107 // always sort the final cursor pos last
108 if (l.kind == TemplateField::FinalCursorPosition) {
109 return false;
110 }
111 if (r.kind == TemplateField::FinalCursorPosition) {
112 return true;
113 }
114 // sort by range
115 return l.range->toRange() < r.range->toRange();
116 });
117}
118
119void KateTemplateHandler::jumpToNextRange()
120{
121 jump(+1);
122}
123
124void KateTemplateHandler::jumpToPreviousRange()
125{
126 jump(-1);
127}
128
129void KateTemplateHandler::jump(int by, bool initial)
130{
131 Q_ASSERT(by == 1 || by == -1);
132 sortFields();
133
134 // find (editable) field index of current cursor position
135 int pos = -1;
136 auto cursor = view()->cursorPosition();
137 // if initial is not set, should start from the beginning (field -1)
138 if (!initial) {
139 const auto range = KTextEditor::Range(cursor, cursor);
140 auto fd = fieldForRange(range);
141 // if nothing was found try to use the next closest range in text
142 if (fd.kind == TemplateField::Invalid) {
143 int i = 0;
144 for (const auto &field : m_fields) {
145 if (!field.removed && field.range->toRange() > range) {
146 pos = i;
147 break;
148 }
149 }
150 } else {
151 pos = m_fields.indexOf(fd);
152 }
153 }
154
155 // modulo field count and make positive
156 auto wrap = [this](int x) -> unsigned int {
157 x %= m_fields.size();
158 return x + (x < 0 ? m_fields.size() : 0);
159 };
160
161 pos = wrap(pos);
162 // choose field to jump to, including wrap-around
163 auto choose_next_field = [this, by, wrap](unsigned int from_field_index) {
164 for (int i = from_field_index + by;; i += by) {
165 auto wrapped_i = wrap(i);
166 const auto &field = m_fields.at(wrapped_i);
167 const auto kind = field.kind;
168 if ((!field.removed && kind == TemplateField::Editable) || kind == TemplateField::FinalCursorPosition) {
169 // found an editable field by walking into the desired direction
170 return wrapped_i;
171 }
172 if (wrapped_i == from_field_index) {
173 // nothing found, do nothing (i.e. keep cursor in current field)
174 break;
175 }
176 }
177 return from_field_index;
178 };
179
180 // jump
181 const auto next = choose_next_field(pos);
182 const auto jump_to_field = m_fields.at(next);
183 view()->setCursorPosition(jump_to_field.range->toRange().start());
184 if (!jump_to_field.touched) {
185 // field was never edited by the user, so select its contents
186 view()->setSelection(jump_to_field.range->toRange());
187 }
188}
189
190void KateTemplateHandler::jumpToFinalCursorPosition()
191{
192 for (const auto &field : std::as_const(m_fields)) {
193 if (field.kind == TemplateField::FinalCursorPosition) {
194 view()->setCursorPosition(field.range->toRange().start());
195 return;
196 }
197 }
198 view()->setCursorPosition(m_wholeTemplateRange->end());
199}
200
201void KateTemplateHandler::slotTemplateInserted(Document * /*document*/, Range range)
202{
203 m_wholeTemplateRange.reset(doc()->newMovingRange(range, MovingRange::ExpandLeft | MovingRange::ExpandRight));
204
205 disconnect(doc(), &KTextEditor::DocumentPrivate::textInsertedRange, this, &KateTemplateHandler::slotTemplateInserted);
206}
207
208KTextEditor::DocumentPrivate *KateTemplateHandler::doc() const
209{
210 return m_view->doc();
211}
212
213void KateTemplateHandler::slotViewCreated(Document *document, View *view)
214{
215 Q_ASSERT(document == doc());
216 Q_UNUSED(document)
217 setupEventHandler(view);
218}
219
220void KateTemplateHandler::setupEventHandler(View *view)
221{
222 view->focusProxy()->installEventFilter(this);
223}
224
226{
227 // prevent indenting by eating the keypress event for TAB
228 if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
229 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
230 if (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab) {
231 if (!m_view->isCompletionActive()) {
232 return true;
233 }
234 }
235 }
236
237 // actually offer shortcuts for navigation
238 if (event->type() == QEvent::ShortcutOverride) {
239 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
240
241 if (keyEvent->key() == Qt::Key_Escape || (keyEvent->key() == Qt::Key_Return && keyEvent->modifiers() & Qt::AltModifier)) {
242 // terminate
243 jumpToFinalCursorPosition();
244 view()->clearSelection();
245 deleteLater();
246 keyEvent->accept();
247 return true;
248 } else if (keyEvent->key() == Qt::Key_Tab && !m_view->isCompletionActive()) {
249 if (keyEvent->modifiers() & Qt::ShiftModifier) {
250 jumpToPreviousRange();
251 } else {
252 jumpToNextRange();
253 }
254 keyEvent->accept();
255 return true;
256 } else if (keyEvent->key() == Qt::Key_Backtab && !m_view->isCompletionActive()) {
257 jumpToPreviousRange();
258 keyEvent->accept();
259 return true;
260 }
261 }
262
263 return QObject::eventFilter(object, event);
264}
265
266/**
267 * Returns an attribute with \p color as background with @p alpha alpha value.
268 */
269Attribute::Ptr getAttribute(QColor color, int alpha = 230)
270{
271 Attribute::Ptr attribute(new Attribute());
272 color.setAlpha(alpha);
273 attribute->setBackground(QBrush(color));
274 return attribute;
275}
276
277void KateTemplateHandler::parseFields(const QString &templateText)
278{
279 // matches any field, i.e. the three forms ${foo}, ${foo=expr}, ${func()}
280 // this also captures escaped fields, i.e. \\${foo} etc.
281 static const QRegularExpression field(QStringLiteral("\\\\?\\${([^}]+)}"), QRegularExpression::UseUnicodePropertiesOption);
282 // matches the "foo=expr" form within a match of the above expression
283 static const QRegularExpression defaultField(QStringLiteral("\\w+=([^\\}]*)"), QRegularExpression::UseUnicodePropertiesOption);
284
285 // compute start cursor of a match
286 auto startOfMatch = [this, &templateText](const QRegularExpressionMatch &match) {
287 const auto offset = match.capturedStart(0);
288 const auto left = QStringView(templateText).left(offset);
289 const auto nl = QLatin1Char('\n');
290 const auto rel_lineno = left.count(nl);
291 const auto start = m_wholeTemplateRange->start().toCursor();
292 return Cursor(start.line(), rel_lineno == 0 ? start.column() : 0) + Cursor(rel_lineno, offset - left.lastIndexOf(nl) - 1);
293 };
294
295 // create a moving range spanning the given field
296 auto createMovingRangeForMatch = [this, startOfMatch](const QRegularExpressionMatch &match) {
297 auto matchStart = startOfMatch(match);
298 return doc()->newMovingRange({matchStart, matchStart + Cursor(0, match.capturedLength(0))}, MovingRange::ExpandLeft | MovingRange::ExpandRight);
299 };
300
301 // list of escape backslashes to remove after parsing
302 QList<KTextEditor::Cursor> stripBackslashes;
303 auto fieldMatch = field.globalMatch(templateText);
304 while (fieldMatch.hasNext()) {
305 const auto match = fieldMatch.next();
306 if (match.captured(0).startsWith(QLatin1Char('\\'))) {
307 // $ is escaped, not a field; mark the backslash for removal
308 // prepend it to the list so the characters are removed starting from the
309 // back and ranges do not move around
310 stripBackslashes.prepend(startOfMatch(match));
311 continue;
312 }
313 // a template field was found, instantiate a field object and populate it
314 auto defaultMatch = defaultField.match(match.captured(0));
315 const QString contents = match.captured(1);
316 TemplateField f;
317 f.range.reset(createMovingRangeForMatch(match));
318 f.identifier = contents;
319 f.kind = TemplateField::Editable;
320 if (defaultMatch.hasMatch()) {
321 // the field has a default value, i.e. ${foo=3}
322 f.defaultValue = defaultMatch.captured(1);
323 f.identifier = QStringView(contents).left(contents.indexOf(QLatin1Char('='))).trimmed().toString();
324 } else if (f.identifier.contains(QLatin1Char('('))) {
325 // field is a function call when it contains an opening parenthesis
326 f.kind = TemplateField::FunctionCall;
327 } else if (f.identifier == QLatin1String("cursor")) {
328 // field marks the final cursor position
329 f.kind = TemplateField::FinalCursorPosition;
330 }
331 for (const auto &other : std::as_const(m_fields)) {
332 if (other.kind == TemplateField::Editable && !(f == other) && other.identifier == f.identifier) {
333 // field is a mirror field
334 f.kind = TemplateField::Mirror;
335 break;
336 }
337 }
338 m_fields.append(f);
339 }
340
341 // remove escape characters
342 for (const auto &backslash : stripBackslashes) {
343 doc()->removeText(KTextEditor::Range(backslash, backslash + Cursor(0, 1)));
344 }
345}
346
347void KateTemplateHandler::setupFieldRanges()
348{
349 auto config = m_view->rendererConfig();
350 auto editableAttribute = getAttribute(config->templateEditablePlaceholderColor(), 255);
351 editableAttribute->setDynamicAttribute(Attribute::ActivateCaretIn, getAttribute(config->templateFocusedEditablePlaceholderColor(), 255));
352 auto notEditableAttribute = getAttribute(config->templateNotEditablePlaceholderColor(), 255);
353
354 // color the whole template
355 m_wholeTemplateRange->setAttribute(getAttribute(config->templateBackgroundColor(), 200));
356
357 // color all the template fields
358 for (const auto &field : std::as_const(m_fields)) {
359 field.range->setAttribute(field.kind == TemplateField::Editable ? editableAttribute : notEditableAttribute);
360 }
361}
362
363void KateTemplateHandler::setupDefaultValues()
364{
365 for (const auto &field : std::as_const(m_fields)) {
366 if (field.kind != TemplateField::Editable) {
367 continue;
368 }
369 QString value;
370 if (field.defaultValue.isEmpty()) {
371 // field has no default value specified; use its identifier
372 value = field.identifier;
373 } else {
374 // field has a default value; evaluate it with the JS engine
375 value = m_templateScript.evaluate(field.defaultValue).toString();
376 }
377 doc()->replaceText(field.range->toRange(), value);
378 }
379}
380
381void KateTemplateHandler::initializeTemplate()
382{
383 auto templateString = doc()->text(*m_wholeTemplateRange);
384 parseFields(templateString);
385 setupFieldRanges();
386 setupDefaultValues();
387
388 // call update for each field to set up the initial stuff
389 for (int i = 0; i < m_fields.size(); i++) {
390 auto &field = m_fields[i];
391 ifDebug(qCDebug(LOG_KTE) << "update field:" << field.range->toRange();) updateDependentFields(doc(), field.range->toRange());
392 // remove "user edited field" mark set by the above call since it's not a real edit
393 field.touched = false;
394 }
395}
396
397const KateTemplateHandler::TemplateField KateTemplateHandler::fieldForRange(KTextEditor::Range range) const
398{
399 for (const auto &field : m_fields) {
400 if (!field.removed && (field.range->contains(range.start()) || field.range->end() == range.start())) {
401 return field;
402 }
403 if (field.kind == TemplateField::FinalCursorPosition && range.end() == field.range->end().toCursor()) {
404 return field;
405 }
406 }
407 return {};
408}
409
410void KateTemplateHandler::updateDependentFields(Document *document, Range range, bool textRemoved)
411{
412 Q_ASSERT(document == doc());
413 Q_UNUSED(document);
414 if (!m_undoManager->isActive()) {
415 // currently undoing stuff; don't update fields
416 return;
417 }
418
419 bool in_range = m_wholeTemplateRange->toRange().contains(range.start());
420 bool at_end = m_wholeTemplateRange->toRange().end() == range.end() || m_wholeTemplateRange->toRange().end() == range.start();
421 if (m_wholeTemplateRange->toRange().isEmpty() || (!in_range && !at_end)) {
422 // edit outside template range, abort
423 ifDebug(qCDebug(LOG_KTE) << "edit outside template range, exiting";) deleteLater();
424 return;
425 }
426
427 if (m_internalEdit || range.isEmpty()) {
428 // internal or null edit; for internal edits, don't do anything
429 // to prevent unwanted recursion
430 return;
431 }
432
433 ifDebug(qCDebug(LOG_KTE) << "text changed" << document << range;)
434
435 // group all the changes into one undo transaction
437
438 // find the field which was modified, if any
439 sortFields();
440 const auto changedField = fieldForRange(range);
441 if (changedField.kind == TemplateField::Invalid) {
442 // edit not within a field, nothing to do
443 ifDebug(qCDebug(LOG_KTE) << "edit not within a field:" << range;) return;
444 }
445 if (changedField.kind == TemplateField::FinalCursorPosition && doc()->text(changedField.range->toRange()).isEmpty()) {
446 // text changed at final cursor position: the user is done, so exit
447 // this is not executed when the field's range is not empty: in that case this call
448 // is for initial setup and we have to continue below
449 ifDebug(qCDebug(LOG_KTE) << "final cursor changed:" << range;) deleteLater();
450 return;
451 }
452
453 if (textRemoved && !range.onSingleLine()) {
454 for (auto &f : m_fields) {
455 if ((f.kind == TemplateField::Editable || f.kind == TemplateField::Mirror) && !f.removed
456 && (range.contains(f.range->toRange()) && f.range->isEmpty())) {
457 f.removed = true;
458 }
459 }
460 }
461
462 // turn off expanding left/right for all ranges except @p current;
463 // this prevents ranges from overlapping each other when they are adjacent
464 auto dontExpandOthers = [this](const TemplateField &current) {
465 for (qsizetype i = 0; i < m_fields.size(); i++) {
466 if (current.range != m_fields.at(i).range) {
467 m_fields.at(i).range->setInsertBehaviors(MovingRange::DoNotExpand);
468 } else {
469 m_fields.at(i).range->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
470 }
471 }
472 };
473
474 // new contents of the changed template field
475 const auto &newText = doc()->text(changedField.range->toRange());
476 m_internalEdit = true;
477 // go through all fields and update the contents of the dependent ones
478 for (auto field = m_fields.begin(); field != m_fields.end(); field++) {
479 if (field->kind == TemplateField::FinalCursorPosition) {
480 // only relevant on first run
481 doc()->replaceText(field->range->toRange(), QString());
482 }
483
484 if (*field == changedField) {
485 // mark that the user changed this field
486 field->touched = true;
487 }
488
489 // If this is mirrored field with the same identifier as the
490 // changed one and the changed one is editable, mirror changes
491 // edits to non-editable mirror fields are ignored
492 if (field->kind == TemplateField::Mirror && changedField.kind == TemplateField::Editable && field->identifier == changedField.identifier) {
493 // editable field changed, mirror changes
494 dontExpandOthers(*field);
495 doc()->replaceText(field->range->toRange(), newText);
496 } else if (field->kind == TemplateField::FunctionCall) {
497 // replace field by result of function call
498 dontExpandOthers(*field);
499 // build map of objects in the scope to pass to the function
500 auto map = fieldMap();
501 const auto &callResult = m_templateScript.evaluate(field->identifier, map);
502 doc()->replaceText(field->range->toRange(), callResult.toString());
503 }
504 }
505 m_internalEdit = false;
506 updateRangeBehaviours();
507}
508
509void KateTemplateHandler::updateRangeBehaviours()
510{
511 KTextEditor::Cursor last = {-1, -1};
512 for (int i = 0; i < m_fields.size(); i++) {
513 auto field = m_fields.at(i);
514 auto end = field.range->end().toCursor();
515 auto start = field.range->start().toCursor();
516 if (field.kind == TemplateField::FinalCursorPosition) {
517 // final cursor position never grows
518 field.range->setInsertBehaviors(MovingRange::DoNotExpand);
519 } else if (start <= last) {
520 // ranges are adjacent, only expand to the right to prevent overlap
521 field.range->setInsertBehaviors(MovingRange::ExpandRight);
522 } else {
523 // ranges are not adjacent, can grow in both directions
524 field.range->setInsertBehaviors(MovingRange::ExpandLeft | MovingRange::ExpandRight);
525 }
526 last = end;
527 }
528}
529
530KateScript::FieldMap KateTemplateHandler::fieldMap() const
531{
533 for (const auto &field : m_fields) {
534 if (field.kind != TemplateField::Editable) {
535 // only editable fields are of interest to the scripts
536 continue;
537 }
538 map.insert(field.identifier, QJSValue(doc()->text(field.range->toRange())));
539 }
540 return map;
541}
542
543KTextEditor::ViewPrivate *KateTemplateHandler::view() const
544{
545 return m_view;
546}
547
548#include "moc_katetemplatehandler.cpp"
A class which provides customized text decorations.
Definition attribute.h:51
@ ActivateCaretIn
Activate attribute on caret in.
Definition attribute.h:248
The Cursor represents a position in a Document.
Definition cursor.h:75
Backend of KTextEditor::Document related public KTextEditor interfaces.
KTextEditor::MovingRange * newMovingRange(KTextEditor::Range range, KTextEditor::MovingRange::InsertBehaviors insertBehaviors=KTextEditor::MovingRange::DoNotExpand, KTextEditor::MovingRange::EmptyBehavior emptyBehavior=KTextEditor::MovingRange::AllowEmpty) override
Create a new moving range for this document.
QString text(KTextEditor::Range range, bool blockwise=false) const override
Get the document content within the given range.
void textRemoved(KTextEditor::Document *document, KTextEditor::Range range, const QString &oldText)
The document emits this signal whenever range was removed, i.e.
void textInsertedRange(KTextEditor::Document *document, KTextEditor::Range range)
The document emits this signal whenever text was inserted.
QList< KTextEditor::View * > views() const override
Returns the views pre-casted to KTextEditor::Views.
Editing transaction support.
Definition document.h:574
A KParts derived class representing a text document.
Definition document.h:284
void viewCreated(KTextEditor::Document *document, KTextEditor::View *view)
This signal is emitted whenever the document creates a new view.
void aboutToReload(KTextEditor::Document *document)
Warn anyone listening that the current document is about to reload.
@ DoNotExpand
Don't expand to encapsulate new characters in either direction. This is the default.
@ ExpandRight
Expand to encapsulate new characters to the right of the range.
@ ExpandLeft
Expand to encapsulate new characters to the left of the range.
An object representing a section of text, from one Cursor to another.
constexpr Cursor end() const noexcept
Get the end position of this range.
constexpr Cursor start() const noexcept
Get the start position of this range.
constexpr bool isEmpty() const noexcept
Returns true if this range contains no characters, ie.
constexpr bool onSingleLine() const noexcept
Check whether this range is wholly contained within one line, ie.
constexpr bool contains(Range range) const noexcept
Check whether the this range wholly encompasses range.
A text widget with KXMLGUIClient that represents a Document.
Definition view.h:244
KateScript objects represent a script that can be executed and inspected.
Definition katescript.h:107
bool setView(KTextEditor::ViewPrivate *view)
set view for this script for the execution will trigger load!
QJSValue evaluate(const QString &program, const FieldMap &env=FieldMap())
Execute a piece of code.
Inserts a template and offers advanced snippet features, like navigation and mirroring.
bool eventFilter(QObject *object, QEvent *event) override
Provide keyboard interaction for the template handler.
KateTemplateHandler(KTextEditor::ViewPrivate *view, KTextEditor::Cursor position, const QString &templateString, const QString &script, KateUndoManager *undoManager)
Setup the template handler, insert the template string.
KateUndoManager implements a document's history.
void setAllowComplexMerge(bool allow)
Allows or disallows merging of "complex" undo groups.
Q_SCRIPTABLE Q_NOREPLY void start()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QAction * next(const QObject *recvr, const char *slot, QObject *parent)
const QList< QKeySequence > & end()
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
void setAlpha(int alpha)
QString toString() const const
iterator begin()
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator end()
void prepend(parameter_type value)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
virtual bool event(QEvent *e)
virtual bool eventFilter(QObject *watched, QEvent *event)
void installEventFilter(QObject *filterObj)
QString & append(QChar ch)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QStringView left(qsizetype length) const const
QString toString() const const
QStringView trimmed() const const
AltModifier
QTextStream & left(QTextStream &stream)
QFuture< void > map(Iterator begin, Iterator end, MapFunctor &&function)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 20 2024 11:56:00 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.