Messagelib

attachmentcontrollerbase.cpp
1 /*
2  * This file is part of KMail.
3  * SPDX-FileCopyrightText: 2009 Constantin Berzan <[email protected]>
4  *
5  * Parts based on KMail code by:
6  * Various authors.
7  *
8  * SPDX-License-Identifier: GPL-2.0-or-later
9  */
10 
11 #include "attachmentcontrollerbase.h"
12 
13 #include "MessageComposer/AttachmentClipBoardJob"
14 #include "MessageComposer/AttachmentFromPublicKeyJob"
15 #include "MessageComposer/AttachmentJob"
16 #include "MessageComposer/AttachmentVcardFromAddressBookJob"
17 #include "MessageComposer/Composer"
18 #include "MessageComposer/GlobalPart"
19 #include <MessageComposer/AttachmentModel>
20 
21 #include <MessageViewer/MessageViewerUtil>
22 
23 #include <MimeTreeParser/NodeHelper>
24 
25 #include <MessageCore/StringUtil>
26 
27 #include <Akonadi/ItemFetchJob>
28 #include <KIO/JobUiDelegateFactory>
29 #include <QIcon>
30 
31 #include <QMenu>
32 #include <QPointer>
33 #include <QTreeView>
34 
35 #include "messagecomposer_debug.h"
36 #include <KActionCollection>
37 #include <KActionMenu>
38 #include <KEncodingFileDialog>
39 #include <KFileItemActions>
40 #include <KIO/ApplicationLauncherJob>
41 #include <KLocalizedString>
42 #include <KMessageBox>
43 #include <QAction>
44 #include <QMimeDatabase>
45 #include <QPushButton>
46 #include <QTemporaryFile>
47 
48 #include <Libkleo/KeySelectionDialog>
49 #include <QGpgME/Protocol>
50 
51 #include "attachment/attachmentfrommimecontentjob.h"
52 #include "attachment/attachmentfromurljob.h"
53 #include "messagecore/attachmentupdatejob.h"
54 #include <Akonadi/EmailAddressSelectionDialog>
55 #include <Akonadi/EmailAddressSelectionWidget>
56 #include <KIO/Job>
57 #include <KIO/JobUiDelegate>
58 #include <KIO/OpenUrlJob>
59 #include <MessageCore/AttachmentCompressJob>
60 #include <MessageCore/AttachmentFromUrlUtils>
61 #include <MessageCore/AttachmentPropertiesDialog>
62 #include <settings/messagecomposersettings.h>
63 
64 #include <KJob>
65 #include <KMime/Content>
66 
67 #include <QActionGroup>
68 #include <QFileDialog>
69 
70 using namespace MessageComposer;
71 using namespace MessageCore;
72 
73 class MessageComposer::AttachmentControllerBase::AttachmentControllerBasePrivate
74 {
75 public:
76  AttachmentControllerBasePrivate(AttachmentControllerBase *qq);
77  ~AttachmentControllerBasePrivate();
78 
79  void attachmentRemoved(const AttachmentPart::Ptr &part); // slot
80  void compressJobResult(KJob *job); // slot
81  void loadJobResult(KJob *job); // slot
82  void openSelectedAttachments(); // slot
83  void viewSelectedAttachments(); // slot
84  void editSelectedAttachment(); // slot
85  void editSelectedAttachmentWith(); // slot
86  void removeSelectedAttachments(); // slot
87  void saveSelectedAttachmentAs(); // slot
88  void selectedAttachmentProperties(); // slot
89  void editDone(MessageViewer::EditorWatcher *watcher); // slot
90  void attachPublicKeyJobResult(KJob *job); // slot
91  void slotAttachmentContentCreated(KJob *job); // slot
92  void addAttachmentPart(AttachmentPart::Ptr part);
93  void attachVcardFromAddressBook(KJob *job); // slot
94  void attachClipBoardElement(KJob *job);
95  void selectedAllAttachment();
96  void createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part);
97  void reloadAttachment();
98  void updateJobResult(KJob *);
99 
100  AttachmentPart::List selectedParts;
101  AttachmentControllerBase *const q;
102  MessageComposer::AttachmentModel *model = nullptr;
103  QWidget *wParent = nullptr;
106 
107  KActionCollection *mActionCollection = nullptr;
108  QAction *attachPublicKeyAction = nullptr;
109  QAction *attachMyPublicKeyAction = nullptr;
110  QAction *openContextAction = nullptr;
111  QAction *viewContextAction = nullptr;
112  QAction *editContextAction = nullptr;
113  QAction *editWithContextAction = nullptr;
114  QAction *removeAction = nullptr;
115  QAction *removeContextAction = nullptr;
116  QAction *saveAsAction = nullptr;
117  QAction *saveAsContextAction = nullptr;
118  QAction *propertiesAction = nullptr;
119  QAction *propertiesContextAction = nullptr;
120  QAction *addAttachmentFileAction = nullptr;
121  QAction *addAttachmentDirectoryAction = nullptr;
122  QAction *addContextAction = nullptr;
123  QAction *selectAllAction = nullptr;
124  KActionMenu *attachmentMenu = nullptr;
125  QAction *addOwnVcardAction = nullptr;
126  QAction *reloadAttachmentAction = nullptr;
127  QAction *attachVCardsAction = nullptr;
128  QAction *attachClipBoardAction = nullptr;
129  // If part p is compressed, uncompressedParts[p] is the uncompressed part.
131  bool encryptEnabled = false;
132  bool signEnabled = false;
133 };
134 
135 AttachmentControllerBase::AttachmentControllerBasePrivate::AttachmentControllerBasePrivate(AttachmentControllerBase *qq)
136  : q(qq)
137 {
138 }
139 
140 AttachmentControllerBase::AttachmentControllerBasePrivate::~AttachmentControllerBasePrivate() = default;
141 
142 void AttachmentControllerBase::setSelectedParts(const AttachmentPart::List &selectedParts)
143 {
144  d->selectedParts = selectedParts;
145  const int selectedCount = selectedParts.count();
146  const bool enableEditAction = (selectedCount == 1) && (!selectedParts.first()->isMessageOrMessageCollection());
147 
148  d->openContextAction->setEnabled(selectedCount > 0);
149  d->viewContextAction->setEnabled(selectedCount > 0);
150  d->editContextAction->setEnabled(enableEditAction);
151  d->editWithContextAction->setEnabled(enableEditAction);
152  d->removeAction->setEnabled(selectedCount > 0);
153  d->removeContextAction->setEnabled(selectedCount > 0);
154  d->saveAsAction->setEnabled(selectedCount == 1);
155  d->saveAsContextAction->setEnabled(selectedCount == 1);
156  d->propertiesAction->setEnabled(selectedCount == 1);
157  d->propertiesContextAction->setEnabled(selectedCount == 1);
158 }
159 
160 void AttachmentControllerBase::AttachmentControllerBasePrivate::attachmentRemoved(const AttachmentPart::Ptr &part)
161 {
162  uncompressedParts.remove(part);
163 }
164 
165 void AttachmentControllerBase::AttachmentControllerBasePrivate::compressJobResult(KJob *job)
166 {
167  if (job->error()) {
168  KMessageBox::error(wParent, job->errorString(), i18n("Failed to compress attachment"));
169  return;
170  }
171 
172  auto ajob = qobject_cast<AttachmentCompressJob *>(job);
173  Q_ASSERT(ajob);
174  AttachmentPart::Ptr originalPart = ajob->originalPart();
175  AttachmentPart::Ptr compressedPart = ajob->compressedPart();
176 
177  if (ajob->isCompressedPartLarger()) {
178  const int result = KMessageBox::questionTwoActions(wParent,
179  i18n("The compressed attachment is larger than the original. "
180  "Do you want to keep the original one?"),
181  QString(/*caption*/),
182  KGuiItem(i18nc("Do not compress", "Keep")),
183  KGuiItem(i18n("Compress")));
184  if (result == KMessageBox::ButtonCode::PrimaryAction) {
185  // The user has chosen to keep the uncompressed file.
186  return;
187  }
188  }
189 
190  qCDebug(MESSAGECOMPOSER_LOG) << "Replacing uncompressed part in model.";
191  uncompressedParts[compressedPart] = originalPart;
192  bool ok = model->replaceAttachment(originalPart, compressedPart);
193  if (!ok) {
194  // The attachment was removed from the model while we were compressing.
195  qCDebug(MESSAGECOMPOSER_LOG) << "Compressed a zombie.";
196  }
197 }
198 
199 void AttachmentControllerBase::AttachmentControllerBasePrivate::loadJobResult(KJob *job)
200 {
201  if (job->error()) {
202  KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach file"));
203  return;
204  }
205 
206  auto ajob = qobject_cast<AttachmentLoadJob *>(job);
207  Q_ASSERT(ajob);
208  AttachmentPart::Ptr part = ajob->attachmentPart();
209  q->addAttachment(part);
210 }
211 
212 void AttachmentControllerBase::AttachmentControllerBasePrivate::openSelectedAttachments()
213 {
214  Q_ASSERT(selectedParts.count() >= 1);
215  for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) {
216  q->openAttachment(part);
217  }
218 }
219 
220 void AttachmentControllerBase::AttachmentControllerBasePrivate::viewSelectedAttachments()
221 {
222  Q_ASSERT(selectedParts.count() >= 1);
223  for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) {
224  q->viewAttachment(part);
225  }
226 }
227 
228 void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachment()
229 {
230  Q_ASSERT(selectedParts.count() == 1);
231  q->editAttachment(selectedParts.constFirst(), MessageViewer::EditorWatcher::NoOpenWithDialog);
232 }
233 
234 void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachmentWith()
235 {
236  Q_ASSERT(selectedParts.count() == 1);
237  q->editAttachment(selectedParts.constFirst(), MessageViewer::EditorWatcher::OpenWithDialog);
238 }
239 
240 void AttachmentControllerBase::AttachmentControllerBasePrivate::removeSelectedAttachments()
241 {
242  Q_ASSERT(selectedParts.count() >= 1);
243  // We must store list, otherwise when we remove it changes selectedParts (as selection changed) => it will crash.
244  const AttachmentPart::List toRemove = selectedParts;
245  for (const AttachmentPart::Ptr &part : toRemove) {
246  model->removeAttachment(part);
247  }
248 }
249 
250 void AttachmentControllerBase::AttachmentControllerBasePrivate::saveSelectedAttachmentAs()
251 {
252  Q_ASSERT(selectedParts.count() == 1);
253  q->saveAttachmentAs(selectedParts.constFirst());
254 }
255 
256 void AttachmentControllerBase::AttachmentControllerBasePrivate::selectedAttachmentProperties()
257 {
258  Q_ASSERT(selectedParts.count() == 1);
259  q->attachmentProperties(selectedParts.constFirst());
260 }
261 
262 void AttachmentControllerBase::AttachmentControllerBasePrivate::reloadAttachment()
263 {
264  Q_ASSERT(selectedParts.count() == 1);
265  auto ajob = new AttachmentUpdateJob(selectedParts.constFirst(), q);
266  connect(ajob, &AttachmentUpdateJob::result, q, [this](KJob *job) {
267  updateJobResult(job);
268  });
269  ajob->start();
270 }
271 
272 void AttachmentControllerBase::AttachmentControllerBasePrivate::updateJobResult(KJob *job)
273 {
274  if (job->error()) {
275  KMessageBox::error(wParent, job->errorString(), i18n("Failed to reload attachment"));
276  return;
277  }
278  auto ajob = qobject_cast<AttachmentUpdateJob *>(job);
279  Q_ASSERT(ajob);
280  AttachmentPart::Ptr originalPart = ajob->originalPart();
281  AttachmentPart::Ptr updatedPart = ajob->updatedPart();
282 
283  attachmentRemoved(originalPart);
284  bool ok = model->replaceAttachment(originalPart, updatedPart);
285  if (!ok) {
286  // The attachment was removed from the model while we were compressing.
287  qCDebug(MESSAGECOMPOSER_LOG) << "Updated a zombie.";
288  }
289 }
290 
291 void AttachmentControllerBase::AttachmentControllerBasePrivate::editDone(MessageViewer::EditorWatcher *watcher)
292 {
293  AttachmentPart::Ptr part = editorPart.take(watcher);
294  Q_ASSERT(part);
295  QTemporaryFile *tempFile = editorTempFile.take(watcher);
296  Q_ASSERT(tempFile);
297  if (watcher->fileChanged()) {
298  qCDebug(MESSAGECOMPOSER_LOG) << "File has changed.";
299  const QString name = watcher->url().path();
300  QFile file(name);
301  if (file.open(QIODevice::ReadOnly)) {
302  const QByteArray data = file.readAll();
303  part->setData(data);
304  model->updateAttachment(part);
305  }
306  }
307 
308  delete tempFile;
309  // The watcher deletes itself.
310 }
311 
312 void AttachmentControllerBase::AttachmentControllerBasePrivate::createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part)
313 {
314  const QString contentTypeStr = QString::fromLatin1(part->mimeType());
315  const KService::List offers = KFileItemActions::associatedApplications(QStringList() << contentTypeStr);
316  if (!offers.isEmpty()) {
317  QMenu *menu = topMenu;
318  auto actionGroup = new QActionGroup(menu);
319  connect(actionGroup, &QActionGroup::triggered, q, &AttachmentControllerBase::slotOpenWithAction);
320 
321  if (offers.count() > 1) { // submenu 'open with'
322  menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu);
323  menu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // for the unittest
324  topMenu->addMenu(menu);
325  }
326  // qCDebug(MESSAGECOMPOSER_LOG) << offers.count() << "offers" << topMenu << menu;
327 
330  for (; it != end; ++it) {
331  QAction *act = MessageViewer::Util::createAppAction(*it,
332  // no submenu -> prefix single offer
333  menu == topMenu,
334  actionGroup,
335  menu);
336  menu->addAction(act);
337  }
338 
339  QString openWithActionName;
340  if (menu != topMenu) { // submenu
341  menu->addSeparator();
342  openWithActionName = i18nc("@action:inmenu Open With", "&Other...");
343  } else {
344  openWithActionName = i18nc("@title:menu", "&Open With...");
345  }
346  auto openWithAct = new QAction(menu);
347  openWithAct->setText(openWithActionName);
348  QObject::connect(openWithAct, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog);
349  menu->addAction(openWithAct);
350  } else { // no app offers -> Open With...
351  auto act = new QAction(topMenu);
352  act->setText(i18nc("@title:menu", "&Open With..."));
353  QObject::connect(act, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog);
354  topMenu->addAction(act);
355  }
356 }
357 
358 void AttachmentControllerBase::exportPublicKey(const QString &fingerprint)
359 {
360  if (fingerprint.isEmpty() || !QGpgME::openpgp()) {
361  qCWarning(MESSAGECOMPOSER_LOG) << "Tried to export key with empty fingerprint, or no OpenPGP.";
362  return;
363  }
364 
365  auto ajob = new MessageComposer::AttachmentFromPublicKeyJob(fingerprint, this);
366  connect(ajob, &AttachmentFromPublicKeyJob::result, this, [this](KJob *job) {
367  d->attachPublicKeyJobResult(job);
368  });
369  ajob->start();
370 }
371 
372 void AttachmentControllerBase::AttachmentControllerBasePrivate::attachPublicKeyJobResult(KJob *job)
373 {
374  // The only reason we can't use loadJobResult() and need a separate method
375  // is that we want to show the proper caption ("public key" instead of "file")...
376 
377  if (job->error()) {
378  KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach public key"));
379  return;
380  }
381 
382  Q_ASSERT(dynamic_cast<MessageComposer::AttachmentFromPublicKeyJob *>(job));
383  auto ajob = static_cast<MessageComposer::AttachmentFromPublicKeyJob *>(job);
384  AttachmentPart::Ptr part = ajob->attachmentPart();
385  q->addAttachment(part);
386 }
387 
388 void AttachmentControllerBase::AttachmentControllerBasePrivate::attachVcardFromAddressBook(KJob *job)
389 {
390  if (job->error()) {
391  qCDebug(MESSAGECOMPOSER_LOG) << " Error during when get vCard";
392  KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach vCard"));
393  return;
394  }
395 
396  auto ajob = static_cast<MessageComposer::AttachmentVcardFromAddressBookJob *>(job);
397  AttachmentPart::Ptr part = ajob->attachmentPart();
398  q->addAttachment(part);
399 }
400 
401 void AttachmentControllerBase::AttachmentControllerBasePrivate::attachClipBoardElement(KJob *job)
402 {
403  if (job->error()) {
404  qCDebug(MESSAGECOMPOSER_LOG) << " Error during when get try to attach text from clipboard";
405  KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach text from clipboard"));
406  return;
407  }
408 
409  auto ajob = static_cast<MessageComposer::AttachmentClipBoardJob *>(job);
410  AttachmentPart::Ptr part = ajob->attachmentPart();
411  q->addAttachment(part);
412 }
413 
414 static QTemporaryFile *dumpAttachmentToTempFile(const AttachmentPart::Ptr &part) // local
415 {
416  auto file = new QTemporaryFile;
417  if (!file->open()) {
418  qCCritical(MESSAGECOMPOSER_LOG) << "Could not open tempfile" << file->fileName();
419  delete file;
420  return nullptr;
421  }
422  if (file->write(part->data()) == -1) {
423  qCCritical(MESSAGECOMPOSER_LOG) << "Could not dump attachment to tempfile.";
424  delete file;
425  return nullptr;
426  }
427  file->flush();
428  return file;
429 }
430 
431 AttachmentControllerBase::AttachmentControllerBase(MessageComposer::AttachmentModel *model, QWidget *wParent, KActionCollection *actionCollection)
432  : QObject(wParent)
433  , d(new AttachmentControllerBasePrivate(this))
434 {
435  d->model = model;
436  connect(model, &MessageComposer::AttachmentModel::attachUrlsRequested, this, &AttachmentControllerBase::addAttachments);
437  connect(model, &MessageComposer::AttachmentModel::attachmentRemoved, this, [this](const MessageCore::AttachmentPart::Ptr &attr) {
438  d->attachmentRemoved(attr);
439  });
440  connect(model, &AttachmentModel::attachmentCompressRequested, this, &AttachmentControllerBase::compressAttachment);
441  connect(model, &MessageComposer::AttachmentModel::encryptEnabled, this, &AttachmentControllerBase::setEncryptEnabled);
442  connect(model, &MessageComposer::AttachmentModel::signEnabled, this, &AttachmentControllerBase::setSignEnabled);
443 
444  d->wParent = wParent;
445  d->mActionCollection = actionCollection;
446 }
447 
448 AttachmentControllerBase::~AttachmentControllerBase() = default;
449 
450 void AttachmentControllerBase::createActions()
451 {
452  // Create the actions.
453  d->attachPublicKeyAction = new QAction(i18n("Attach &Public Key..."), this);
454  connect(d->attachPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachPublicKeyDialog);
455 
456  d->attachMyPublicKeyAction = new QAction(i18n("Attach &My Public Key"), this);
457  connect(d->attachMyPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::attachMyPublicKey);
458 
459  d->attachmentMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Attach"), this);
460  connect(d->attachmentMenu, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
461  d->attachmentMenu->setPopupMode(QToolButton::DelayedPopup);
462 
463  d->addAttachmentFileAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach File..."), this);
464  d->addAttachmentFileAction->setIconText(i18n("Attach"));
465  d->addContextAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Add Attachment..."), this);
466  connect(d->addAttachmentFileAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
467  connect(d->addContextAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog);
468 
469  d->addAttachmentDirectoryAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Directory..."), this);
470  d->addAttachmentDirectoryAction->setIconText(i18n("Attach"));
471  connect(d->addAttachmentDirectoryAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog);
472 
473  d->addOwnVcardAction = new QAction(i18n("Attach Own vCard"), this);
474  d->addOwnVcardAction->setIconText(i18n("Own vCard"));
475  d->addOwnVcardAction->setCheckable(true);
476  connect(d->addOwnVcardAction, &QAction::triggered, this, &AttachmentControllerBase::addOwnVcard);
477 
478  d->attachVCardsAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach vCards..."), this);
479  d->attachVCardsAction->setIconText(i18n("Attach"));
480  connect(d->attachVCardsAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachVcard);
481 
482  d->attachClipBoardAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Text From Clipboard..."), this);
483  d->attachClipBoardAction->setIconText(i18n("Attach Text From Clipboard"));
484  connect(d->attachClipBoardAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachClipBoard);
485 
486  d->attachmentMenu->addAction(d->addAttachmentFileAction);
487  d->attachmentMenu->addAction(d->addAttachmentDirectoryAction);
488  d->attachmentMenu->addSeparator();
489  d->attachmentMenu->addAction(d->addOwnVcardAction);
490  d->attachmentMenu->addSeparator();
491  d->attachmentMenu->addAction(d->attachVCardsAction);
492  d->attachmentMenu->addSeparator();
493  d->attachmentMenu->addAction(d->attachClipBoardAction);
494 
495  d->removeAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Remove Attachment"), this);
496  d->removeContextAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove"), this); // FIXME need two texts. is there a better way?
497  connect(d->removeAction, &QAction::triggered, this, [this]() {
498  d->removeSelectedAttachments();
499  });
500  connect(d->removeContextAction, &QAction::triggered, this, [this]() {
501  d->removeSelectedAttachments();
502  });
503 
504  d->openContextAction = new QAction(i18nc("to open", "Open"), this);
505  connect(d->openContextAction, &QAction::triggered, this, [this]() {
506  d->openSelectedAttachments();
507  });
508 
509  d->viewContextAction = new QAction(i18nc("to view", "View"), this);
510  connect(d->viewContextAction, &QAction::triggered, this, [this]() {
511  d->viewSelectedAttachments();
512  });
513 
514  d->editContextAction = new QAction(i18nc("to edit", "Edit"), this);
515  connect(d->editContextAction, &QAction::triggered, this, [this]() {
516  d->editSelectedAttachment();
517  });
518 
519  d->editWithContextAction = new QAction(i18n("Edit With..."), this);
520  connect(d->editWithContextAction, &QAction::triggered, this, [this]() {
521  d->editSelectedAttachmentWith();
522  });
523 
524  d->saveAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), this);
525  d->saveAsContextAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."), this);
526  connect(d->saveAsAction, &QAction::triggered, this, [this]() {
527  d->saveSelectedAttachmentAs();
528  });
529  connect(d->saveAsContextAction, &QAction::triggered, this, [this]() {
530  d->saveSelectedAttachmentAs();
531  });
532 
533  d->propertiesAction = new QAction(i18n("Attachment Pr&operties..."), this);
534  d->propertiesContextAction = new QAction(i18n("Properties"), this);
535  connect(d->propertiesAction, &QAction::triggered, this, [this]() {
536  d->selectedAttachmentProperties();
537  });
538  connect(d->propertiesContextAction, &QAction::triggered, this, [this]() {
539  d->selectedAttachmentProperties();
540  });
541 
542  d->selectAllAction = new QAction(i18n("Select All"), this);
543  connect(d->selectAllAction, &QAction::triggered, this, &AttachmentControllerBase::selectedAllAttachment);
544 
545  d->reloadAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reload"), this);
546  connect(d->reloadAttachmentAction, &QAction::triggered, this, [this]() {
547  d->reloadAttachment();
548  });
549 
550  // Insert the actions into the composer window's menu.
551  KActionCollection *collection = d->mActionCollection;
552  collection->addAction(QStringLiteral("attach_public_key"), d->attachPublicKeyAction);
553  collection->addAction(QStringLiteral("attach_my_public_key"), d->attachMyPublicKeyAction);
554  collection->addAction(QStringLiteral("attach"), d->addAttachmentFileAction);
555  collection->setDefaultShortcut(d->addAttachmentFileAction, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A));
556  collection->addAction(QStringLiteral("attach_directory"), d->addAttachmentDirectoryAction);
557 
558  collection->addAction(QStringLiteral("remove"), d->removeAction);
559  collection->addAction(QStringLiteral("attach_save"), d->saveAsAction);
560  collection->addAction(QStringLiteral("attach_properties"), d->propertiesAction);
561  collection->addAction(QStringLiteral("select_all_attachment"), d->selectAllAction);
562  collection->addAction(QStringLiteral("attach_menu"), d->attachmentMenu);
563  collection->addAction(QStringLiteral("attach_own_vcard"), d->addOwnVcardAction);
564  collection->addAction(QStringLiteral("attach_vcards"), d->attachVCardsAction);
565 
566  setSelectedParts(AttachmentPart::List());
567  Q_EMIT actionsCreated();
568 }
569 
570 void AttachmentControllerBase::setEncryptEnabled(bool enabled)
571 {
572  d->encryptEnabled = enabled;
573 }
574 
575 void AttachmentControllerBase::setSignEnabled(bool enabled)
576 {
577  d->signEnabled = enabled;
578 }
579 
580 void AttachmentControllerBase::compressAttachment(const AttachmentPart::Ptr &part, bool compress)
581 {
582  if (compress) {
583  qCDebug(MESSAGECOMPOSER_LOG) << "Compressing part.";
584 
585  auto ajob = new AttachmentCompressJob(part, this);
586  connect(ajob, &AttachmentCompressJob::result, this, [this](KJob *job) {
587  d->compressJobResult(job);
588  });
589  ajob->start();
590  } else {
591  qCDebug(MESSAGECOMPOSER_LOG) << "Uncompressing part.";
592 
593  // Replace the compressed part with the original uncompressed part, and delete
594  // the compressed part.
595  AttachmentPart::Ptr originalPart = d->uncompressedParts.take(part);
596  Q_ASSERT(originalPart); // Found in uncompressedParts.
597  bool ok = d->model->replaceAttachment(part, originalPart);
598  Q_ASSERT(ok);
599  Q_UNUSED(ok)
600  }
601 }
602 
603 void AttachmentControllerBase::showContextMenu()
604 {
605  Q_EMIT refreshSelection();
606 
607  const int numberOfParts(d->selectedParts.count());
608  QMenu menu;
609 
610  const bool enableEditAction = (numberOfParts == 1) && (!d->selectedParts.first()->isMessageOrMessageCollection());
611 
612  if (numberOfParts > 0) {
613  if (numberOfParts == 1) {
614  const QString mimetype = QString::fromLatin1(d->selectedParts.first()->mimeType());
615  QMimeDatabase mimeDb;
616  auto mime = mimeDb.mimeTypeForName(mimetype);
617  QStringList parentMimeType;
618  if (mime.isValid()) {
619  parentMimeType = mime.allAncestors();
620  }
621  if ((mimetype == QLatin1String("text/plain")) || (mimetype == QLatin1String("image/png")) || (mimetype == QLatin1String("image/jpeg"))
622  || parentMimeType.contains(QLatin1String("text/plain")) || parentMimeType.contains(QLatin1String("image/png"))
623  || parentMimeType.contains(QLatin1String("image/jpeg"))) {
624  menu.addAction(d->viewContextAction);
625  }
626  d->createOpenWithMenu(&menu, d->selectedParts.constFirst());
627  }
628  menu.addAction(d->openContextAction);
629  }
630  if (enableEditAction) {
631  menu.addAction(d->editWithContextAction);
632  menu.addAction(d->editContextAction);
633  }
634  menu.addSeparator();
635  if (numberOfParts == 1) {
636  if (!d->selectedParts.first()->url().isEmpty()) {
637  menu.addAction(d->reloadAttachmentAction);
638  }
639  menu.addAction(d->saveAsContextAction);
640  menu.addSeparator();
641  menu.addAction(d->propertiesContextAction);
642  menu.addSeparator();
643  }
644 
645  if (numberOfParts > 0) {
646  menu.addAction(d->removeContextAction);
647  menu.addSeparator();
648  }
649  const int nbAttachment = d->model->rowCount();
650  if (nbAttachment != numberOfParts) {
651  menu.addAction(d->selectAllAction);
652  menu.addSeparator();
653  }
654  if (numberOfParts == 0) {
655  menu.addAction(d->addContextAction);
656  }
657 
658  menu.exec(QCursor::pos());
659 }
660 
661 void AttachmentControllerBase::slotOpenWithDialog()
662 {
663  openWith();
664 }
665 
666 void AttachmentControllerBase::slotOpenWithAction(QAction *act)
667 {
668  auto app = act->data().value<KService::Ptr>();
669  Q_ASSERT(d->selectedParts.count() == 1);
670 
671  openWith(app);
672 }
673 
674 void AttachmentControllerBase::openWith(const KService::Ptr &offer)
675 {
676  QTemporaryFile *tempFile = dumpAttachmentToTempFile(d->selectedParts.constFirst());
677  if (!tempFile) {
678  KMessageBox::error(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18n("Unable to open attachment"));
679  return;
680  }
681  QUrl url = QUrl::fromLocalFile(tempFile->fileName());
682  tempFile->setPermissions(QFile::ReadUser);
683  // If offer is null, this will show the "open with" dialog
684  auto job = new KIO::ApplicationLauncherJob(offer);
685  job->setUrls({url});
686  job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent));
687  job->start();
688  connect(job, &KJob::result, this, [tempFile, job]() {
689  if (job->error()) {
690  delete tempFile;
691  }
692  });
693  // Delete the file only when the composer is closed
694  // (and this object is destroyed).
695  tempFile->setParent(this); // Manages lifetime.
696 }
697 
698 void AttachmentControllerBase::openAttachment(const AttachmentPart::Ptr &part)
699 {
700  QTemporaryFile *tempFile = dumpAttachmentToTempFile(part);
701  if (!tempFile) {
702  KMessageBox::error(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18n("Unable to open attachment"));
703  return;
704  }
705  tempFile->setPermissions(QFile::ReadUser);
706  auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()));
707  job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent));
708  job->setDeleteTemporaryFile(true);
709  connect(job, &KIO::OpenUrlJob::result, this, [this, tempFile](KJob *job) {
710  if (job->error() == KIO::ERR_USER_CANCELED) {
711  KMessageBox::error(d->wParent, i18n("KMail was unable to open the attachment."), job->errorString());
712  delete tempFile;
713  } else {
714  // The file was opened. Delete it only when the composer is closed
715  // (and this object is destroyed).
716  tempFile->setParent(this); // Manages lifetime.
717  }
718  });
719  job->start();
720 }
721 
722 void AttachmentControllerBase::viewAttachment(const AttachmentPart::Ptr &part)
723 {
724  auto composer = new MessageComposer::Composer;
725  composer->globalPart()->setFallbackCharsetEnabled(true);
726  auto attachmentJob = new MessageComposer::AttachmentJob(part, composer);
727  connect(attachmentJob, &AttachmentJob::result, this, [this](KJob *job) {
728  d->slotAttachmentContentCreated(job);
729  });
730  attachmentJob->start();
731 }
732 
733 void AttachmentControllerBase::AttachmentControllerBasePrivate::slotAttachmentContentCreated(KJob *job)
734 {
735  if (!job->error()) {
736  const MessageComposer::AttachmentJob *const attachmentJob = qobject_cast<MessageComposer::AttachmentJob *>(job);
737  Q_ASSERT(attachmentJob);
738  if (attachmentJob) {
739  Q_EMIT q->showAttachment(attachmentJob->content(), QByteArray());
740  }
741  } else {
742  // TODO: show warning to the user
743  qCWarning(MESSAGECOMPOSER_LOG) << "Error creating KMime::Content for attachment:" << job->errorText();
744  }
745 }
746 
747 void AttachmentControllerBase::editAttachment(AttachmentPart::Ptr part, MessageViewer::EditorWatcher::OpenWithOption openWithOption)
748 {
749  QTemporaryFile *tempFile = dumpAttachmentToTempFile(part);
750  if (!tempFile) {
751  KMessageBox::error(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18n("Unable to edit attachment"));
752  return;
753  }
754 
755  auto watcher =
756  new MessageViewer::EditorWatcher(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()), openWithOption, this, d->wParent);
757  connect(watcher, &MessageViewer::EditorWatcher::editDone, this, [this](MessageViewer::EditorWatcher *watcher) {
758  d->editDone(watcher);
759  });
760 
761  switch (watcher->start()) {
762  case MessageViewer::EditorWatcher::NoError:
763  // The attachment is being edited.
764  // We will clean things up in editDone().
765  d->editorPart[watcher] = part;
766  d->editorTempFile[watcher] = tempFile;
767 
768  // Delete the temp file if the composer is closed (and this object is destroyed).
769  tempFile->setParent(this); // Manages lifetime.
770  break;
771  case MessageViewer::EditorWatcher::CannotStart:
772  qCWarning(MESSAGECOMPOSER_LOG) << "Could not start EditorWatcher.";
773  Q_FALLTHROUGH();
774  case MessageViewer::EditorWatcher::Unknown:
775  case MessageViewer::EditorWatcher::Canceled:
776  case MessageViewer::EditorWatcher::NoServiceFound:
777  delete watcher;
778  delete tempFile;
779  break;
780  }
781 }
782 
783 void AttachmentControllerBase::editAttachmentWith(const AttachmentPart::Ptr &part)
784 {
785  editAttachment(part, MessageViewer::EditorWatcher::OpenWithDialog);
786 }
787 
788 void AttachmentControllerBase::saveAttachmentAs(const AttachmentPart::Ptr &part)
789 {
790  QString pname = part->name();
791  if (pname.isEmpty()) {
792  pname = i18n("unnamed");
793  }
794 
795  const QUrl url = QFileDialog::getSaveFileUrl(d->wParent, i18n("Save Attachment As"), QUrl::fromLocalFile(pname));
796 
797  if (url.isEmpty()) {
798  qCDebug(MESSAGECOMPOSER_LOG) << "Save Attachment As dialog canceled.";
799  return;
800  }
801 
802  byteArrayToRemoteFile(part->data(), url);
803 }
804 
805 void AttachmentControllerBase::byteArrayToRemoteFile(const QByteArray &aData, const QUrl &aURL, bool overwrite)
806 {
807  KIO::StoredTransferJob *job = KIO::storedPut(aData, aURL, -1, overwrite ? KIO::Overwrite : KIO::DefaultFlags);
808  connect(job, &KIO::StoredTransferJob::result, this, &AttachmentControllerBase::slotPutResult);
809 }
810 
811 void AttachmentControllerBase::slotPutResult(KJob *job)
812 {
813  auto _job = qobject_cast<KIO::StoredTransferJob *>(job);
814 
815  if (job->error()) {
816  if (job->error() == KIO::ERR_FILE_ALREADY_EXIST) {
818  i18n("File %1 exists.\nDo you want to replace it?", _job->url().toLocalFile()),
819  i18n("Save to File"),
820  KGuiItem(i18n("&Replace")))
821  == KMessageBox::Continue) {
822  byteArrayToRemoteFile(_job->data(), _job->url(), true);
823  }
824  } else {
825  KJobUiDelegate *ui = static_cast<KIO::Job *>(job)->uiDelegate();
826  ui->showErrorMessage();
827  }
828  }
829 }
830 
831 void AttachmentControllerBase::attachmentProperties(const AttachmentPart::Ptr &part)
832 {
833  QPointer<AttachmentPropertiesDialog> dialog = new AttachmentPropertiesDialog(part, false, d->wParent);
834 
835  dialog->setEncryptEnabled(d->encryptEnabled);
836  dialog->setSignEnabled(d->signEnabled);
837 
838  if (dialog->exec() && dialog) {
839  d->model->updateAttachment(part);
840  }
841  delete dialog;
842 }
843 
844 void AttachmentControllerBase::attachDirectory(const QUrl &url)
845 {
846  const int rc = KMessageBox::warningTwoActions(d->wParent,
847  i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()),
848  i18nc("@title:window", "Attach directory"),
849  KGuiItem(i18nc("@action:button", "Attach")),
851  if (rc == KMessageBox::ButtonCode::PrimaryAction) {
852  addAttachment(url);
853  }
854 }
855 
856 void AttachmentControllerBase::showAttachVcard()
857 {
859  dlg->view()->view()->setSelectionMode(QAbstractItemView::MultiSelection);
860  if (dlg->exec()) {
861  const Akonadi::EmailAddressSelection::List selectedEmail = dlg->selectedAddresses();
862  for (const Akonadi::EmailAddressSelection &selected : selectedEmail) {
863  auto ajob = new MessageComposer::AttachmentVcardFromAddressBookJob(selected.item(), this);
864  connect(ajob, &AttachmentVcardFromAddressBookJob::result, this, [this](KJob *job) {
865  d->attachVcardFromAddressBook(job);
866  });
867  ajob->start();
868  }
869  }
870  delete dlg;
871 }
872 
873 void AttachmentControllerBase::showAttachClipBoard()
874 {
875  auto job = new MessageComposer::AttachmentClipBoardJob(this);
876  connect(job, &AttachmentClipBoardJob::result, this, [this](KJob *job) {
877  d->attachClipBoardElement(job);
878  });
879  job->start();
880 }
881 
882 void AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog()
883 {
884  const QUrl url = QFileDialog::getExistingDirectoryUrl(d->wParent, i18nc("@title:window", "Attach Directory"));
885  if (url.isValid()) {
886  attachDirectory(url);
887  }
888 }
889 
890 void AttachmentControllerBase::showAddAttachmentFileDialog()
891 {
892  const KEncodingFileDialog::Result result =
893  KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), QUrl(), QString(), d->wParent, i18nc("@title:window", "Attach File"));
894  if (!result.URLs.isEmpty()) {
895  const QString encoding = MimeTreeParser::NodeHelper::fixEncoding(result.encoding);
896  const int numberOfFiles(result.URLs.count());
897  for (int i = 0; i < numberOfFiles; ++i) {
898  const QUrl url = result.URLs.at(i);
899  QUrl urlWithEncoding = url;
900  MessageCore::StringUtil::setEncodingFile(urlWithEncoding, encoding);
901  QMimeDatabase mimeDb;
902  const auto mimeType = mimeDb.mimeTypeForUrl(urlWithEncoding);
903  if (mimeType.name() == QLatin1String("inode/directory")) {
904  const int rc = KMessageBox::warningTwoActions(d->wParent,
905  i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()),
906  i18nc("@title:window", "Attach directory"),
907  KGuiItem(i18nc("@action:button", "Attach")),
909  if (rc == KMessageBox::ButtonCode::PrimaryAction) {
910  addAttachment(urlWithEncoding);
911  }
912  } else {
913  addAttachment(urlWithEncoding);
914  }
915  }
916  }
917 }
918 
919 void AttachmentControllerBase::addAttachment(const AttachmentPart::Ptr &part)
920 {
921  part->setEncrypted(d->model->isEncryptSelected());
922  part->setSigned(d->model->isSignSelected());
923  d->model->addAttachment(part);
924 
925  Q_EMIT fileAttached();
926 }
927 
928 void AttachmentControllerBase::addAttachmentUrlSync(const QUrl &url)
929 {
930  MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this);
931  if (ajob->exec()) {
932  AttachmentPart::Ptr part = ajob->attachmentPart();
933  addAttachment(part);
934  } else {
935  if (ajob->error()) {
936  KMessageBox::error(d->wParent, ajob->errorString(), i18nc("@title:window", "Failed to attach file"));
937  }
938  }
939 }
940 
941 void AttachmentControllerBase::addAttachment(const QUrl &url)
942 {
943  MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this);
944  connect(ajob, &AttachmentFromUrlBaseJob::result, this, [this](KJob *job) {
945  d->loadJobResult(job);
946  });
947  ajob->start();
948 }
949 
950 void AttachmentControllerBase::addAttachments(const QList<QUrl> &urls)
951 {
952  for (const QUrl &url : urls) {
953  addAttachment(url);
954  }
955 }
956 
957 void AttachmentControllerBase::showAttachPublicKeyDialog()
958 {
959  using Kleo::KeySelectionDialog;
960  QPointer<KeySelectionDialog> dialog = new KeySelectionDialog(i18n("Attach Public OpenPGP Key"),
961  i18n("Select the public key which should be attached."),
962  std::vector<GpgME::Key>(),
963  KeySelectionDialog::PublicKeys | KeySelectionDialog::OpenPGPKeys,
964  false /* no multi selection */,
965  false /* no remember choice box */,
966  d->wParent);
967 
968  if (dialog->exec() == QDialog::Accepted) {
969  exportPublicKey(dialog->fingerprint());
970  }
971  delete dialog;
972 }
973 
974 void AttachmentControllerBase::attachMyPublicKey()
975 {
976 }
977 
978 void AttachmentControllerBase::enableAttachPublicKey(bool enable)
979 {
980  d->attachPublicKeyAction->setEnabled(enable);
981 }
982 
983 void AttachmentControllerBase::enableAttachMyPublicKey(bool enable)
984 {
985  d->attachMyPublicKeyAction->setEnabled(enable);
986 }
987 
988 void AttachmentControllerBase::setAttachOwnVcard(bool attachVcard)
989 {
990  d->addOwnVcardAction->setChecked(attachVcard);
991 }
992 
993 bool AttachmentControllerBase::attachOwnVcard() const
994 {
995  return d->addOwnVcardAction->isChecked();
996 }
997 
998 void AttachmentControllerBase::setIdentityHasOwnVcard(bool state)
999 {
1000  d->addOwnVcardAction->setEnabled(state);
1001 }
1002 
1003 #include "moc_attachmentcontrollerbase.cpp"
T & first()
QAction * addAction(const QString &name, const QObject *receiver=nullptr, const char *member=nullptr)
T * data() const const
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
A job to compress the attachment of an email.
virtual Q_SCRIPTABLE void start()=0
QMimeType mimeTypeForUrl(const QUrl &url) const const
ButtonCode warningContinueCancel(QWidget *parent, const QString &text, const QString &title=QString(), const KGuiItem &buttonContinue=KStandardGuiItem::cont(), const KGuiItem &buttonCancel=KStandardGuiItem::cancel(), const QString &dontAskAgainName=QString(), Options options=Notify)
void result(KJob *job)
The AttachmentJob class.
Definition: attachmentjob.h:21
int count(const T &value) const const
T value() const const
void setDefaultShortcut(QAction *action, const QKeySequence &shortcut)
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
KIOCORE_EXPORT StoredTransferJob * storedPut(QIODevice *input, const QUrl &url, int permissions, JobFlags flags=DefaultFlags)
QIcon fromTheme(const QString &name)
virtual bool setPermissions(QFileDevice::Permissions permissions) override
QAction * addSeparator()
Starts an editor for the given URL and emits an signal when editing has been finished.
Definition: editorwatcher.h:25
The Composer class.
Definition: composer.h:33
KCALUTILS_EXPORT QString mimeType()
QList::const_iterator constBegin() const const
virtual void showErrorMessage()
static KIOFILEWIDGETS_EXPORT Result getOpenUrlsAndEncoding(const QString &encoding=QString(), const QUrl &startDir=QUrl(), const QString &filter=QString(), QWidget *parent=nullptr, const QString &title=QString())
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
virtual QString fileName() const const override
static KService::List associatedApplications(const QStringList &mimeTypeList)
QAction * addAction(const QString &text)
KGuiItem cancel()
bool isValid() const const
QString i18n(const char *text, const TYPE &arg...)
bool isEmpty() const const
The AttachmentClipBoardJob class.
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
KMime::Content * content() const
Get the resulting KMime::Content that the ContentJobBase has generated.
bool isEmpty() const const
QUrl getSaveFileUrl(QWidget *parent, const QString &caption, const QUrl &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options, const QStringList &supportedSchemes)
QUrl fromLocalFile(const QString &localFile)
QAction * addMenu(QMenu *menu)
QString errorText() const
void setText(const QString &text)
bool isEmpty() const const
QString toLocalFile() const const
A dialog for editing attachment properties.
ButtonCode warningTwoActions(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const QString &dontAskAgainName=QString(), Options options=Options(Notify|Dangerous))
QPoint pos()
ButtonCode questionTwoActions(QWidget *parent, const QString &text, const QString &title, const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const QString &dontAskAgainName=QString(), Options options=Notify)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QVariant data() const const
typedef ConstIterator
void triggered(bool checked)
The AttachmentControllerBase class.
QUrl getExistingDirectoryUrl(QWidget *parent, const QString &caption, const QUrl &dir, QFileDialog::Options options, const QStringList &supportedSchemes)
QList::const_iterator constEnd() const const
QString path(QUrl::ComponentFormattingOptions options) const const
void triggered(QAction *action)
static QString fixEncoding(const QString &encoding)
Fixes an encoding received by a KDE function and returns the proper, MIME-compliant encoding name ins...
QString fromLatin1(const char *str, int size)
void setObjectName(const QString &name)
QString name(StandardShortcut id)
void setParent(QObject *parent)
void setUiDelegate(KJobUiDelegate *delegate)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
The AttachmentModel class.
const T & constFirst() const const
virtual QString errorString() const
int error() const
QAction * menuAction() const const
const QList< QKeySequence > & end()
QAction * exec()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Mar 26 2023 04:08:10 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.