1 /*
2 SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
3 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8 #include "incidenceattachment.h"
9 #include "attachmenteditdialog.h"
10 #include "attachmenticonview.h"
11 #include "ui_dialogdesktop.h"
12
13 #include <CalendarSupport/UriHandler>
14
15 #include <KContacts/VCardDrag>
16
17 #include <KMime/Message>
18
19 #include <KActionCollection>
20 #include <KIO/Job>
21 #include <KIO/JobUiDelegate>
22 #include <KIO/OpenUrlJob>
23 #include <KIO/StoredTransferJob>
24 #include <KJobWidgets>
25 #include <KLocalizedString>
26 #include <KMessageBox>
27 #include <KProtocolManager>
28 #include <QAction>
29 #include <QFileDialog>
30 #include <QIcon>
31 #include <QMenu>
32 #include <QUrl>
33
34 #include <QClipboard>
35 #include <QMimeData>
36 #include <QMimeDatabase>
37 #include <QMimeType>
38
39 using namespace IncidenceEditorNG;
40
IncidenceAttachment(Ui::EventOrTodoDesktop * ui)41 IncidenceAttachment::IncidenceAttachment(Ui::EventOrTodoDesktop *ui)
42 : IncidenceEditor(nullptr)
43 , mUi(ui)
44 , mPopupMenu(new QMenu)
45 {
46 setupActions();
47 setupAttachmentIconView();
48 setObjectName(QStringLiteral("IncidenceAttachment"));
49
50 connect(mUi->mAddButton, &QPushButton::clicked, this, &IncidenceAttachment::addAttachment);
51 connect(mUi->mRemoveButton, &QPushButton::clicked, this, &IncidenceAttachment::removeSelectedAttachments);
52 }
53
~IncidenceAttachment()54 IncidenceAttachment::~IncidenceAttachment()
55 {
56 delete mPopupMenu;
57 }
58
load(const KCalendarCore::Incidence::Ptr & incidence)59 void IncidenceAttachment::load(const KCalendarCore::Incidence::Ptr &incidence)
60 {
61 mLoadedIncidence = incidence;
62 mAttachmentView->clear();
63
64 KCalendarCore::Attachment::List attachments = incidence->attachments();
65 for (KCalendarCore::Attachment::List::ConstIterator it = attachments.constBegin(), end = attachments.constEnd(); it != end; ++it) {
66 new AttachmentIconItem((*it), mAttachmentView);
67 }
68
69 mWasDirty = false;
70 }
71
save(const KCalendarCore::Incidence::Ptr & incidence)72 void IncidenceAttachment::save(const KCalendarCore::Incidence::Ptr &incidence)
73 {
74 incidence->clearAttachments();
75
76 for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
77 QListWidgetItem *item = mAttachmentView->item(itemIndex);
78 auto attitem = dynamic_cast<AttachmentIconItem *>(item);
79 Q_ASSERT(item);
80 incidence->addAttachment(attitem->attachment());
81 }
82 }
83
isDirty() const84 bool IncidenceAttachment::isDirty() const
85 {
86 if (mLoadedIncidence) {
87 if (mAttachmentView->count() != mLoadedIncidence->attachments().count()) {
88 return true;
89 }
90
91 KCalendarCore::Attachment::List origAttachments = mLoadedIncidence->attachments();
92 for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
93 QListWidgetItem *item = mAttachmentView->item(itemIndex);
94 Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
95
96 const KCalendarCore::Attachment listAttachment = static_cast<AttachmentIconItem *>(item)->attachment();
97
98 for (int i = 0; i < origAttachments.count(); ++i) {
99 const KCalendarCore::Attachment attachment = origAttachments.at(i);
100
101 if (attachment == listAttachment) {
102 origAttachments.remove(i);
103 break;
104 }
105 }
106 }
107 // All attachments are removed from the list, meaning, the items in mAttachmentView
108 // are equal to the attachments set on mLoadedIncidence.
109 return !origAttachments.isEmpty();
110 } else {
111 // No incidence loaded, so if the user added attachments we're dirty.
112 return mAttachmentView->count() != 0;
113 }
114 }
115
attachmentCount() const116 int IncidenceAttachment::attachmentCount() const
117 {
118 return mAttachmentView->count();
119 }
120
121 /// Private slots
122
addAttachment()123 void IncidenceAttachment::addAttachment()
124 {
125 QPointer<QObject> that(this);
126 auto item = new AttachmentIconItem(KCalendarCore::Attachment(), mAttachmentView);
127
128 QPointer<AttachmentEditDialog> dialog(new AttachmentEditDialog(item, mAttachmentView));
129 dialog->setWindowTitle(i18nc("@title", "Add Attachment"));
130 auto dialogResult = dialog->exec();
131 if (!that) {
132 return;
133 }
134
135 if (dialogResult == QDialog::Rejected) {
136 delete item;
137 } else {
138 Q_EMIT attachmentCountChanged(mAttachmentView->count());
139 }
140 delete dialog;
141
142 checkDirtyStatus();
143 }
144
copyToClipboard()145 void IncidenceAttachment::copyToClipboard()
146 {
147 #ifndef QT_NO_CLIPBOARD
148 QApplication::clipboard()->setMimeData(mAttachmentView->mimeData(), QClipboard::Clipboard);
149 #endif
150 }
151
openURL(const QUrl & url)152 void IncidenceAttachment::openURL(const QUrl &url)
153 {
154 QString uri = url.url();
155 CalendarSupport::UriHandler::process(uri);
156 }
157
pasteFromClipboard()158 void IncidenceAttachment::pasteFromClipboard()
159 {
160 #ifndef QT_NO_CLIPBOARD
161 handlePasteOrDrop(QApplication::clipboard()->mimeData());
162 #endif
163 }
164
removeSelectedAttachments()165 void IncidenceAttachment::removeSelectedAttachments()
166 {
167 QList<QListWidgetItem *> selected;
168 QStringList labels;
169 selected.reserve(mAttachmentView->count());
170 labels.reserve(mAttachmentView->count());
171
172 for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
173 QListWidgetItem *it = mAttachmentView->item(itemIndex);
174 if (it->isSelected()) {
175 auto attitem = static_cast<AttachmentIconItem *>(it);
176 if (attitem) {
177 const KCalendarCore::Attachment att = attitem->attachment();
178 labels << att.label();
179 selected << it;
180 }
181 }
182 }
183
184 if (selected.isEmpty()) {
185 return;
186 }
187
188 QString labelsStr = labels.join(QLatin1String("<nl/>"));
189
190 if (KMessageBox::questionYesNo(nullptr,
191 xi18nc("@info", "Do you really want to remove these attachments?<nl/>%1", labelsStr),
192 i18nc("@title:window", "Remove Attachments?"),
193 KStandardGuiItem::remove(),
194 KStandardGuiItem::cancel(),
195 QStringLiteral("calendarRemoveAttachments"))
196 != KMessageBox::Yes) {
197 return;
198 }
199
200 for (QList<QListWidgetItem *>::iterator it(selected.begin()), end(selected.end()); it != end; ++it) {
201 int row = mAttachmentView->row(*it);
202 QListWidgetItem *next = mAttachmentView->item(++row);
203 QListWidgetItem *prev = mAttachmentView->item(--row);
204 if (next) {
205 next->setSelected(true);
206 } else if (prev) {
207 prev->setSelected(true);
208 }
209 delete *it;
210 }
211
212 mAttachmentView->update();
213 Q_EMIT attachmentCountChanged(mAttachmentView->count());
214 checkDirtyStatus();
215 }
216
saveAttachment(QListWidgetItem * item)217 void IncidenceAttachment::saveAttachment(QListWidgetItem *item)
218 {
219 Q_ASSERT(item);
220 Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
221
222 auto attitem = static_cast<AttachmentIconItem *>(item);
223 if (attitem->attachment().isEmpty()) {
224 return;
225 }
226
227 KCalendarCore::Attachment att = attitem->attachment();
228
229 // get the saveas file name
230 const QString saveAsFile = QFileDialog::getSaveFileName(nullptr, i18nc("@title", "Save Attachment"), att.label());
231
232 if (saveAsFile.isEmpty()) {
233 return;
234 }
235
236 QUrl sourceUrl;
237 if (att.isUri()) {
238 sourceUrl = QUrl(att.uri());
239 } else {
240 sourceUrl = attitem->tempFileForAttachment();
241 }
242 // save the attachment url
243 auto job = KIO::file_copy(sourceUrl, QUrl::fromLocalFile(saveAsFile));
244 if (!job->exec() && job->error()) {
245 KMessageBox::error(nullptr, job->errorString());
246 }
247 }
248
saveSelectedAttachments()249 void IncidenceAttachment::saveSelectedAttachments()
250 {
251 for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
252 QListWidgetItem *item = mAttachmentView->item(itemIndex);
253 if (item->isSelected()) {
254 saveAttachment(item);
255 }
256 }
257 }
258
showAttachment(QListWidgetItem * item)259 void IncidenceAttachment::showAttachment(QListWidgetItem *item)
260 {
261 Q_ASSERT(item);
262 Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
263 auto attitem = static_cast<AttachmentIconItem *>(item);
264 if (attitem->attachment().isEmpty()) {
265 return;
266 }
267
268 const KCalendarCore::Attachment att = attitem->attachment();
269 if (att.isUri()) {
270 openURL(QUrl(att.uri()));
271 } else {
272 auto job = new KIO::OpenUrlJob(attitem->tempFileForAttachment(), att.mimeType());
273 job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, mAttachmentView));
274 job->setDeleteTemporaryFile(true);
275 job->start();
276 }
277 }
278
showContextMenu(const QPoint & pos)279 void IncidenceAttachment::showContextMenu(const QPoint &pos) // clazy:exclude=function-args-by-value
280 {
281 const bool enable = mAttachmentView->itemAt(pos) != nullptr;
282
283 int numSelected = 0;
284 for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
285 QListWidgetItem *item = mAttachmentView->item(itemIndex);
286 if (item->isSelected()) {
287 numSelected++;
288 }
289 }
290
291 mOpenAction->setEnabled(enable);
292 // TODO: support saving multiple attachments into a directory
293 mSaveAsAction->setEnabled(enable && numSelected == 1);
294 #ifndef QT_NO_CLIPBOARD
295 mCopyAction->setEnabled(enable && numSelected == 1);
296 mCutAction->setEnabled(enable && numSelected == 1);
297 #endif
298 mDeleteAction->setEnabled(enable);
299 mEditAction->setEnabled(enable);
300 mPopupMenu->exec(mAttachmentView->mapToGlobal(pos));
301 }
302
showSelectedAttachments()303 void IncidenceAttachment::showSelectedAttachments()
304 {
305 for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
306 QListWidgetItem *item = mAttachmentView->item(itemIndex);
307 if (item->isSelected()) {
308 showAttachment(item);
309 }
310 }
311 }
312
cutToClipboard()313 void IncidenceAttachment::cutToClipboard()
314 {
315 #ifndef QT_NO_CLIPBOARD
316 copyToClipboard();
317 removeSelectedAttachments();
318 #endif
319 }
320
editSelectedAttachments()321 void IncidenceAttachment::editSelectedAttachments()
322 {
323 for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
324 QListWidgetItem *item = mAttachmentView->item(itemIndex);
325 if (item->isSelected()) {
326 Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
327
328 auto attitem = static_cast<AttachmentIconItem *>(item);
329 if (attitem->attachment().isEmpty()) {
330 return;
331 }
332
333 QPointer<AttachmentEditDialog> dialog(new AttachmentEditDialog(attitem, mAttachmentView, false));
334 dialog->setModal(false);
335 dialog->setAttribute(Qt::WA_DeleteOnClose, true);
336 dialog->show();
337 }
338 }
339 }
340
slotItemRenamed(QListWidgetItem * item)341 void IncidenceAttachment::slotItemRenamed(QListWidgetItem *item)
342 {
343 Q_ASSERT(item);
344 Q_ASSERT(dynamic_cast<AttachmentIconItem *>(item));
345 static_cast<AttachmentIconItem *>(item)->setLabel(item->text());
346 checkDirtyStatus();
347 }
348
slotSelectionChanged()349 void IncidenceAttachment::slotSelectionChanged()
350 {
351 bool selected = false;
352 for (int itemIndex = 0; itemIndex < mAttachmentView->count(); ++itemIndex) {
353 QListWidgetItem *item = mAttachmentView->item(itemIndex);
354 if (item->isSelected()) {
355 selected = true;
356 break;
357 }
358 }
359 mUi->mRemoveButton->setEnabled(selected);
360 }
361
362 /// Private functions
363
handlePasteOrDrop(const QMimeData * mimeData)364 void IncidenceAttachment::handlePasteOrDrop(const QMimeData *mimeData)
365 {
366 if (!mimeData) {
367 return;
368 }
369 QList<QUrl> urls;
370 bool probablyWeHaveUris = false;
371 QStringList labels;
372
373 if (KContacts::VCardDrag::canDecode(mimeData)) {
374 KContacts::Addressee::List addressees;
375 KContacts::VCardDrag::fromMimeData(mimeData, addressees);
376 urls.reserve(addressees.count());
377 labels.reserve(addressees.count());
378 const KContacts::Addressee::List::ConstIterator end(addressees.constEnd());
379 for (KContacts::Addressee::List::ConstIterator it = addressees.constBegin(); it != end; ++it) {
380 urls.append(QUrl(QStringLiteral("uid:") + (*it).uid()));
381 // there is some weirdness about realName(), hence fromUtf8
382 labels.append(QString::fromUtf8((*it).realName().toLatin1()));
383 }
384 probablyWeHaveUris = true;
385 } else if (mimeData->hasUrls()) {
386 QMap<QString, QString> metadata;
387
388 // QT5
389 // urls = QList<QUrl>::fromMimeData( mimeData, &metadata );
390 probablyWeHaveUris = true;
391 labels = metadata[QStringLiteral("labels")].split(QLatin1Char(':'), Qt::SkipEmptyParts);
392 const QStringList::Iterator end(labels.end());
393 for (QStringList::Iterator it = labels.begin(); it != end; ++it) {
394 *it = QUrl::fromPercentEncoding((*it).toLatin1());
395 }
396 } else if (mimeData->hasText()) {
397 const QString text = mimeData->text();
398 QStringList lst = text.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
399 urls.reserve(lst.count());
400 QStringList::ConstIterator end(lst.constEnd());
401 for (QStringList::ConstIterator it = lst.constBegin(); it != end; ++it) {
402 urls.append(QUrl(*it));
403 }
404 probablyWeHaveUris = true;
405 }
406 QMenu menu;
407 QAction *linkAction = nullptr;
408 QAction *cancelAction = nullptr;
409 if (probablyWeHaveUris) {
410 linkAction = menu.addAction(QIcon::fromTheme(QStringLiteral("insert-link")), i18nc("@action:inmenu", "&Link here"));
411 // we need to check if we can reasonably expect to copy the objects
412 bool weCanCopy = true;
413 QList<QUrl>::ConstIterator end(urls.constEnd());
414 for (QList<QUrl>::ConstIterator it = urls.constBegin(); it != end; ++it) {
415 if (!(weCanCopy = KProtocolManager::supportsReading(*it))) {
416 break; // either we can copy them all, or no copying at all
417 }
418 }
419 if (weCanCopy) {
420 menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action:inmenu", "&Copy here"));
421 }
422 } else {
423 menu.addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action:inmenu", "&Copy here"));
424 }
425
426 menu.addSeparator();
427 cancelAction = menu.addAction(QIcon::fromTheme(QStringLiteral("process-stop")), i18nc("@action:inmenu", "C&ancel"));
428
429 QByteArray data;
430 QString mimeType;
431 QString label;
432
433 if (!mimeData->formats().isEmpty() && !probablyWeHaveUris) {
434 mimeType = mimeData->formats().first();
435 data = mimeData->data(mimeType);
436 QMimeDatabase db;
437 QMimeType mime = db.mimeTypeForName(mimeType);
438 if (mime.isValid()) {
439 label = mime.comment();
440 }
441 }
442
443 QAction *ret = menu.exec(QCursor::pos());
444 if (linkAction == ret) {
445 QStringList::ConstIterator jt = labels.constBegin();
446 const QList<QUrl>::ConstIterator jtEnd = urls.constEnd();
447 for (QList<QUrl>::ConstIterator it = urls.constBegin(); it != jtEnd; ++it) {
448 addUriAttachment((*it).url(), QString(), (jt == labels.constEnd() ? QString() : *(jt++)), true);
449 }
450 } else if (cancelAction != ret) {
451 if (probablyWeHaveUris) {
452 QList<QUrl>::ConstIterator end = urls.constEnd();
453 for (QList<QUrl>::ConstIterator it = urls.constBegin(); it != end; ++it) {
454 KIO::Job *job = KIO::storedGet(*it);
455 // TODO verify if slot exist !
456 connect(job, &KIO::Job::result, this, &IncidenceAttachment::downloadComplete);
457 }
458 } else { // we take anything
459 addDataAttachment(data, mimeType, label);
460 }
461 }
462 }
463
downloadComplete(KJob *)464 void IncidenceAttachment::downloadComplete(KJob *)
465 {
466 // TODO
467 }
468
setupActions()469 void IncidenceAttachment::setupActions()
470 {
471 auto ac = new KActionCollection(this);
472 // ac->addAssociatedWidget( this );
473
474 mOpenAction = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18nc("@action:inmenu open the attachment in a viewer", "&Open"), this);
475 connect(mOpenAction, &QAction::triggered, this, &IncidenceAttachment::showSelectedAttachments);
476 ac->addAction(QStringLiteral("view"), mOpenAction);
477 mPopupMenu->addAction(mOpenAction);
478
479 mSaveAsAction =
480 new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18nc("@action:inmenu save the attachment to a file", "Save As..."), this);
481 connect(mSaveAsAction, &QAction::triggered, this, &IncidenceAttachment::saveSelectedAttachments);
482 mPopupMenu->addAction(mSaveAsAction);
483 mPopupMenu->addSeparator();
484
485 #ifndef QT_NO_CLIPBOARD
486 mCopyAction = KStandardAction::copy(this, &IncidenceAttachment::copyToClipboard, ac);
487 mPopupMenu->addAction(mCopyAction);
488
489 mCutAction = KStandardAction::cut(this, &IncidenceAttachment::cutToClipboard, ac);
490 mPopupMenu->addAction(mCutAction);
491
492 QAction *action = KStandardAction::paste(this, &IncidenceAttachment::pasteFromClipboard, ac);
493 mPopupMenu->addAction(action);
494 mPopupMenu->addSeparator();
495 #endif
496
497 mDeleteAction = new QAction(QIcon::fromTheme(QStringLiteral("list-remove")), i18nc("@action:inmenu remove the attachment", "&Remove"), this);
498 connect(mDeleteAction, &QAction::triggered, this, &IncidenceAttachment::removeSelectedAttachments);
499 ac->addAction(QStringLiteral("remove"), mDeleteAction);
500 mDeleteAction->setShortcut(Qt::Key_Delete);
501 mPopupMenu->addAction(mDeleteAction);
502 mPopupMenu->addSeparator();
503
504 mEditAction = new QAction(QIcon::fromTheme(QStringLiteral("document-properties")),
505 i18nc("@action:inmenu show a dialog used to edit the attachment", "&Properties..."),
506 this);
507 connect(mEditAction, &QAction::triggered, this, &IncidenceAttachment::editSelectedAttachments);
508 ac->addAction(QStringLiteral("edit"), mEditAction);
509 mPopupMenu->addAction(mEditAction);
510 }
511
setupAttachmentIconView()512 void IncidenceAttachment::setupAttachmentIconView()
513 {
514 mAttachmentView = new AttachmentIconView;
515 mAttachmentView->setWhatsThis(i18nc("@info:whatsthis",
516 "Displays items (files, mail, etc.) that "
517 "have been associated with this event or to-do."));
518
519 connect(mAttachmentView, &AttachmentIconView::itemDoubleClicked, this, &IncidenceAttachment::showAttachment);
520 connect(mAttachmentView, &AttachmentIconView::itemChanged, this, &IncidenceAttachment::slotItemRenamed);
521 connect(mAttachmentView, &AttachmentIconView::itemSelectionChanged, this, &IncidenceAttachment::slotSelectionChanged);
522 connect(mAttachmentView, &AttachmentIconView::customContextMenuRequested, this, &IncidenceAttachment::showContextMenu);
523
524 auto layout = new QGridLayout(mUi->mAttachmentViewPlaceHolder);
525 layout->setContentsMargins(0, 0, 0, 0);
526 layout->addWidget(mAttachmentView);
527 }
528
529 // void IncidenceAttachmentEditor::addAttachment( KCalendarCore::Attachment *attachment )
530 // {
531 // new AttachmentIconItem( attachment, mAttachmentView );
532 // }
533
addDataAttachment(const QByteArray & data,const QString & mimeType,const QString & label)534 void IncidenceAttachment::addDataAttachment(const QByteArray &data, const QString &mimeType, const QString &label)
535 {
536 auto item = new AttachmentIconItem(KCalendarCore::Attachment(), mAttachmentView);
537
538 QString nlabel = label;
539 if (mimeType == QLatin1String("message/rfc822")) {
540 // mail message. try to set the label from the mail Subject:
541 KMime::Message msg;
542 msg.setContent(data);
543 msg.parse();
544 nlabel = msg.subject()->asUnicodeString();
545 }
546
547 item->setData(data);
548 item->setLabel(nlabel);
549 if (mimeType.isEmpty()) {
550 QMimeDatabase db;
551 item->setMimeType(db.mimeTypeForData(data).name());
552 } else {
553 item->setMimeType(mimeType);
554 }
555
556 checkDirtyStatus();
557 }
558
addUriAttachment(const QString & uri,const QString & mimeType,const QString & label,bool inLine)559 void IncidenceAttachment::addUriAttachment(const QString &uri, const QString &mimeType, const QString &label, bool inLine)
560 {
561 if (!inLine) {
562 auto item = new AttachmentIconItem(KCalendarCore::Attachment(), mAttachmentView);
563 item->setUri(uri);
564 item->setLabel(label);
565 if (mimeType.isEmpty()) {
566 if (uri.startsWith(QLatin1String("uid:"))) {
567 item->setMimeType(QStringLiteral("text/directory"));
568 } else if (uri.startsWith(QLatin1String("kmail:"))) {
569 item->setMimeType(QStringLiteral("message/rfc822"));
570 } else if (uri.startsWith(QLatin1String("urn:x-ical"))) {
571 item->setMimeType(QStringLiteral("text/calendar"));
572 } else if (uri.startsWith(QLatin1String("news:"))) {
573 item->setMimeType(QStringLiteral("message/news"));
574 } else {
575 QMimeDatabase db;
576 item->setMimeType(db.mimeTypeForUrl(QUrl(uri)).name());
577 }
578 }
579 } else {
580 auto job = KIO::storedGet(QUrl(uri));
581 KJobWidgets::setWindow(job, nullptr);
582 if (job->exec()) {
583 const QByteArray data = job->data();
584 addDataAttachment(data, mimeType, label);
585 }
586 }
587 }
588