Commit 1e35dd37 authored by David Jarvie's avatar David Jarvie
Browse files

Allow drag-and-drop of email onto display alarm text edit field

parent 4efe300d
Pipeline #23762 passed with stage
in 12 minutes and 28 seconds
KAlarm Change Log
=== Version 3.0.0 --- 7 June 2020 ===
=== Version 3.0.0 --- 15 June 2020 ===
+ Provide option to use file system resources instead of Akonadi resources.
+ Enable selection of multiple calendar files in Import Alarms dialogue.
+ Show alarm calendars sorted by name in calendars list.
+ Return to last used tab in Configuration dialogue when it is reopened.
+ Fix handling of calendar update or save errors when making alarm changes.
+ Allow drag-and-drop of email onto display alarm text edit field.
+ Refactor AlarmCalendar to split out resources and display calendars.
=== Version 2.14.2 (KDE Applications 20.04.2) --- 8 June 2020 ===
......
/*
* editdlg_p.h - private classes for editdlg.cpp
* Program: kalarm
* Copyright © 2003-2005,2007-2009 by David Jarvie <djarvie@kde.org>
* Copyright © 2003-2020 David Jarvie <djarvie@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
......@@ -47,11 +47,17 @@ class TextEdit : public KTextEdit
Q_OBJECT
public:
explicit TextEdit(QWidget* parent);
void enableEmailDrop();
QSize sizeHint() const override { return minimumSizeHint(); }
QSize minimumSizeHint() const override { return minimumSize(); }
protected:
void dragEnterEvent(QDragEnterEvent*) override;
void dragMoveEvent(QDragMoveEvent*) override;
void dropEvent(QDropEvent*) override;
private:
bool mEmailDrop {false};
};
class CommandEdit : public QWidget
......
/*
* editdlgtypes.cpp - dialogs to create or edit alarm or alarm template types
* Program: kalarm
* Copyright © 2001-2019 David Jarvie <djarvie@kde.org>
* Copyright © 2001-2020 David Jarvie <djarvie@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
......@@ -23,6 +23,7 @@
#include "emailidcombo.h"
#include "fontcolourbutton.h"
#include "functions.h"
#include "kalarmapp.h"
#include "kamail.h"
#include "latecancel.h"
......@@ -179,6 +180,7 @@ void EditDisplayAlarmDlg::type_init(QWidget* parent, QVBoxLayout* frameLayout)
// Text message edit box
mTextMessageEdit = new TextEdit(parent);
mTextMessageEdit->setLineWrapMode(KTextEdit::NoWrap);
mTextMessageEdit->enableEmailDrop(); // allow drag-and-drop of emails onto this widget
mTextMessageEdit->setWhatsThis(i18nc("@info:whatsthis", "Enter the text of the alarm message. It may be multi-line."));
connect(mTextMessageEdit, &TextEdit::textChanged, this, &EditDisplayAlarmDlg::contentsChanged);
frameLayout->addWidget(mTextMessageEdit);
......@@ -1865,11 +1867,72 @@ TextEdit::TextEdit(QWidget* parent)
setMinimumSize(tsize);
}
void TextEdit::enableEmailDrop()
{
mEmailDrop = true;
setAcceptDrops(true); // allow drag-and-drop onto this widget
}
void TextEdit::dragEnterEvent(QDragEnterEvent* e)
{
if (KCalUtils::ICalDrag::canDecode(e->mimeData()))
{
e->ignore(); // don't accept "text/calendar" objects
return;
}
if (mEmailDrop && KAlarm::mayHaveRFC822(e->mimeData()))
{
e->acceptProposedAction();
return;
}
KTextEdit::dragEnterEvent(e);
}
void TextEdit::dragMoveEvent(QDragMoveEvent* e)
{
if (mEmailDrop && KAlarm::mayHaveRFC822(e->mimeData()))
{
e->acceptProposedAction();
return;
}
KTextEdit::dragMoveEvent(e);
}
/******************************************************************************
* Called when an object is dropped on the widget.
*/
void TextEdit::dropEvent(QDropEvent* e)
{
if (mEmailDrop)
{
const QMimeData* data = e->mimeData();
AlarmText alarmText;
bool haveEmail = false;
if (KAlarm::dropRFC822(data, alarmText))
{
// Email message(s). Ignore all but the first.
qCDebug(KALARM_LOG) << "TextEdit::dropEvent: email";
haveEmail = true;
}
else
{
QUrl url;
Akonadi::Item item;
if (KAlarm::dropAkonadiEmail(data, url, item, alarmText))
{
// It's an email held in Akonadi
qCDebug(KALARM_LOG) << "TextEdit::dropEvent: Akonadi email";
haveEmail = true;
}
}
if (haveEmail)
{
if (!alarmText.isEmpty())
setPlainText(alarmText.displayText());
return;
}
}
KTextEdit::dropEvent(e);
}
// vim: et sw=4:
......@@ -47,6 +47,9 @@
#include <KAlarmCal/Identities>
#include <KAlarmCal/KAEvent>
#include <AkonadiCore/ItemFetchJob>
#include <AkonadiCore/ItemFetchScope>
#include <KMime/Message>
#include <KCalendarCore/Event>
#include <KCalendarCore/ICalFormat>
#include <KCalendarCore/Person>
......@@ -79,6 +82,8 @@ using namespace KCalendarCore;
#include <QStandardPaths>
#include <QPushButton>
#include <QTemporaryFile>
#include <QMimeData>
#include <QUrlQuery>
namespace
......@@ -145,6 +150,8 @@ const char* DONT_SHOW_ERRORS_GROUP = "DontShowErrors";
KAlarm::UpdateResult updateEvent(KAEvent&, KAlarm::UpdateError, QWidget* msgParent);
void editNewTemplate(EditAlarmDlg::Type, const KAEvent* preset, QWidget* parent);
void displayUpdateError(QWidget* parent, KAlarm::UpdateError, const UpdateStatusData&, bool showKOrgError = true);
KAlarmCal::AlarmText kMimeEmailToAlarmText(KMime::Content&, Akonadi::Item::Id);
QString getMailHeader(const char* header, KMime::Content&);
KAlarm::UpdateResult sendToKOrganizer(const KAEvent&);
KAlarm::UpdateResult deleteFromKOrganizer(const QString& eventID);
KAlarm::UpdateResult runKOrganizer();
......@@ -1640,6 +1647,82 @@ void refreshAlarmsIfQueued()
}
}
/******************************************************************************
* Check whether drag-and-drop data may contain an RFC822 message (Akonadi or not).
*/
bool mayHaveRFC822(const QMimeData* data)
{
return data->hasFormat(QStringLiteral("message/rfc822"))
|| data->hasUrls();
}
/******************************************************************************
* Extract dragged and dropped RFC822 message data.
*/
bool dropRFC822(const QMimeData* data, KAlarmCal::AlarmText& alarmText)
{
const QByteArray bytes = data->data(QStringLiteral("message/rfc822"));
if (bytes.isEmpty())
{
alarmText.clear();
return false;
}
// Email message(s). Ignore all but the first.
qCDebug(KALARM_LOG) << "KAlarm::dropRFC822: have email";
KMime::Content content;
content.setContent(bytes);
content.parse();
alarmText = kMimeEmailToAlarmText(content, -1);
return true;
}
/******************************************************************************
* Extract dragged and dropped Akonadi RFC822 message data.
*/
bool dropAkonadiEmail(const QMimeData* data, QUrl& url, Akonadi::Item& item, KAlarmCal::AlarmText& alarmText)
{
alarmText.clear();
const QList<QUrl> urls = data->urls();
if (urls.isEmpty())
{
url = QUrl();
item.setId(-1);
return false;
}
url = urls.at(0);
item = Akonadi::Item::fromUrl(url);
if (!item.isValid())
return false;
// It's an Akonadi item
qCDebug(KALARM_LOG) << "KAlarm::dropAkonadiEmail: Akonadi item" << item.id();
if (QUrlQuery(url).queryItemValue(QStringLiteral("type")) != QLatin1String("message/rfc822"))
return false;
// It's an email held in Akonadi
qCDebug(KALARM_LOG) << "KAlarm::dropAkonadiEmail: Akonadi email";
Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(item);
job->fetchScope().fetchFullPayload();
Akonadi::Item::List items;
if (job->exec())
items = job->items();
if (items.isEmpty())
qCWarning(KALARM_LOG) << "KAlarm::dropAkonadiEmail: Akonadi item" << item.id() << "not found";
else
{
const Akonadi::Item& it = items.at(0);
if (!it.isValid() || !it.hasPayload<KMime::Message::Ptr>())
qCWarning(KALARM_LOG) << "KAlarm::dropAkonadiEmail: invalid email";
else
{
KMime::Message::Ptr message = it.payload<KMime::Message::Ptr>();
alarmText = kMimeEmailToAlarmText(*message, it.id());
}
}
return true;
}
/******************************************************************************
* Start KMail if it isn't already running, optionally minimised.
* Reply = reason for failure to run KMail
......@@ -1976,6 +2059,31 @@ void displayUpdateError(QWidget* parent, KAlarm::UpdateError code, const UpdateS
displayKOrgUpdateError(parent, code, status.status, status.warnKOrg);
}
/******************************************************************************
* Convert a KMime email instance to AlarmText.
*/
KAlarmCal::AlarmText kMimeEmailToAlarmText(KMime::Content& content, Akonadi::Item::Id itemId)
{
QString body;
if (content.textContent())
body = content.textContent()->decodedText(true, true); // strip trailing newlines & spaces
AlarmText alarmText;
alarmText.setEmail(getMailHeader("To", content),
getMailHeader("From", content),
getMailHeader("Cc", content),
getMailHeader("Date", content),
getMailHeader("Subject", content),
body,
itemId);
return alarmText;
}
QString getMailHeader(const char* header, KMime::Content& content)
{
KMime::Headers::Base* hd = content.headerByType(header);
return hd ? hd->asUnicodeString() : QString();
}
/******************************************************************************
* Tell KOrganizer to put an alarm in its calendar.
* It will be held by KOrganizer as a simple event, without alarms - KAlarm
......
......@@ -36,6 +36,7 @@ using namespace KAlarmCal;
namespace KCal { class Event; }
class QWidget;
class QAction;
class QMimeData;
class KToggleAction;
class Resource;
class MainWindow;
......@@ -103,6 +104,29 @@ KAEvent::List templateList();
void outputAlarmWarnings(QWidget* parent, const KAEvent* = nullptr);
void refreshAlarms();
void refreshAlarmsIfQueued(); // must only be called from KAlarmApp::processQueue()
/** Check whether drag-and-drop data may contain an RFC822 message (Akonadi or not). */
bool mayHaveRFC822(const QMimeData* data);
/** Extract dragged and dropped RFC822 message data.
* If there is more than one message, only the first is extracted.
* @param data Dropped data.
* @param alarmText Extracted email data.
* @return true if @p data contained RFC822 message data, false if not.
*/
bool dropRFC822(const QMimeData* data, KAlarmCal::AlarmText& alarmText);
/** Extract dragged and dropped Akonadi RFC822 message data.
* @param data Dropped data.
* @param url Receives the first URL in the data, or empty if data does
* not provide URLs.
* @param item Receives the Akonadi Item specified by @p url, or invalid
* if not an Akonadi URL.
* @param alarmText Extracted email data.
* @return true if @p data contained RFC822 message data, false if not.
*/
bool dropAkonadiEmail(const QMimeData* data, QUrl& url, Akonadi::Item& item, KAlarmCal::AlarmText& alarmText);
QString runKMail();
QStringList dontShowErrors(const EventId&);
......
......@@ -867,6 +867,7 @@ void KAlarmApp::checkNextDueAlarm()
/* TODO: Use hibernation wakeup signal:
* #include <Solid/Power>
* connect(Solid::Power::self(), &Solid::Power::resumeFromSuspend, ...)
* (or resumingFromSuspend?)
* to be notified when wakeup from hibernation occurs. But can't use it
* unless we know that this notification is supported by the system!
*/
......
......@@ -47,10 +47,7 @@
#include <KAlarmCal/AlarmText>
#include <KAlarmCal/KAEvent>
#include <KMime/Message>
#include <AkonadiCore/Item>
#include <AkonadiCore/ItemFetchJob>
#include <AkonadiCore/ItemFetchScope>
#include <KCalendarCore/MemoryCalendar>
#include <KCalUtils/ICalDrag>
using namespace KCalendarCore;
......@@ -83,7 +80,6 @@ using namespace KCalUtils;
#include <QMimeDatabase>
#include <QInputDialog>
#include <QUrl>
#include <QUrlQuery>
#include <QMenuBar>
#include <QSystemTrayIcon>
#include <QMimeData>
......@@ -1200,8 +1196,7 @@ void MainWindow::executeDragEnterEvent(QDragEnterEvent* e)
{
const QMimeData* data = e->mimeData();
bool accept = ICalDrag::canDecode(data) ? !e->source() // don't accept "text/calendar" objects from this application
: data->hasText()
|| data->hasUrls();
: data->hasText() || data->hasUrls();
if (accept)
e->acceptProposedAction();
}
......@@ -1215,12 +1210,6 @@ void MainWindow::dropEvent(QDropEvent* e)
executeDropEvent(this, e);
}
static QString getMailHeader(const char* header, KMime::Content& content)
{
KMime::Headers::Base* hd = content.headerByType(header);
return hd ? hd->asUnicodeString() : QString();
}
/******************************************************************************
* Called when an object is dropped on a main or system tray window, to
* evaluate the action required and extract the text.
......@@ -1230,7 +1219,6 @@ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e)
qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Formats:" << e->mimeData()->formats();
const QMimeData* data = e->mimeData();
KAEvent::SubAction action = KAEvent::MESSAGE;
QByteArray bytes;
AlarmText alarmText;
QList<QUrl> urls;
MemoryCalendar::Ptr calendar(new MemoryCalendar(Preferences::timeSpecAsZone()));
......@@ -1243,23 +1231,11 @@ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e)
* provide more than one mime type.
* Don't change them without careful thought !!
*/
if (!(bytes = data->data(QStringLiteral("message/rfc822"))).isEmpty())
if (KAlarm::dropRFC822(data, alarmText))
{
// Email message(s). Ignore all but the first.
qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: email";
KMime::Content content;
content.setContent(bytes);
content.parse();
QString body;
if (content.textContent())
body = content.textContent()->decodedText(true, true); // strip trailing newlines & spaces
unsigned long sernum = 0;
alarmText.setEmail(getMailHeader("To", content),
getMailHeader("From", content),
getMailHeader("Cc", content),
getMailHeader("Date", content),
getMailHeader("Subject", content),
body, sernum);
//TODO: Fetch attachments if an email alarm is created below
}
else if (ICalDrag::fromMimeData(data, calendar))
{
......@@ -1334,48 +1310,19 @@ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e)
}
return;
}
else if (!(urls = data->urls()).isEmpty())
else
{
const QUrl& url(urls.at(0));
const Akonadi::Item item = Akonadi::Item::fromUrl(url);
if (item.isValid())
QUrl url;
Akonadi::Item item;
if (KAlarm::dropAkonadiEmail(data, url, item, alarmText))
{
// It's an Akonadi item
qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi item" << item.id();
if (QUrlQuery(url).queryItemValue(QStringLiteral("type")) == QLatin1String("message/rfc822"))
{
// It's an email held in Akonadi
qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi email";
Akonadi::ItemFetchJob* job = new Akonadi::ItemFetchJob(item);
job->fetchScope().fetchFullPayload();
Akonadi::Item::List items;
if (job->exec())
items = job->items();
if (items.isEmpty())
{
qCWarning(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi item" << item.id() << "not found";
return;
}
const Akonadi::Item& it = items.at(0);
if (!it.isValid() || !it.hasPayload<KMime::Message::Ptr>())
{
qCWarning(KALARM_LOG) << "MainWindow::executeDropEvent: invalid email";
return;
}
KMime::Message::Ptr message = it.payload<KMime::Message::Ptr>();
QString body;
if (message->textContent())
body = message->textContent()->decodedText(true, true); // strip trailing newlines & spaces
alarmText.setEmail(getMailHeader("To", *message),
getMailHeader("From", *message),
getMailHeader("Cc", *message),
getMailHeader("Date", *message),
getMailHeader("Subject", *message),
body, it.id());
}
// It's an email held in Akonadi
qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: Akonadi email";
//TODO: Fetch attachments if an email alarm is created below
}
else
else if (!url.isEmpty() && !item.isValid())
{
// The data provides a URL, but it isn't an Akonadi URL.
qCDebug(KALARM_LOG) << "MainWindow::executeDropEvent: URL";
// Try to find the mime type of the file, without downloading a remote file
QMimeDatabase mimeDb;
......@@ -1410,8 +1357,8 @@ void MainWindow::executeDropEvent(MainWindow* win, QDropEvent* e)
else if (alarmText.isScript())
types += i18nc("@item:inlistbox", "Command Alarm");
bool ok = false;
QString type = QInputDialog::getItem(mainMainWindow(), i18nc("@title:window", "Alarm Type"),
i18nc("@info", "Choose alarm type to create:"), types, 0, false, &ok);
const QString type = QInputDialog::getItem(mainMainWindow(), i18nc("@title:window", "Alarm Type"),
i18nc("@info", "Choose alarm type to create:"), types, 0, false, &ok);
if (!ok)
return; // user didn't press OK
int i = types.indexOf(type);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment