Commit 639ab997 authored by Daniel Vrátil's avatar Daniel Vrátil 🤖

Initial version of a PIM events plugin for Plasma Clock Applet

This implements a plugin through ETMCalendar. It eats a lot of
memory. The UI in Plasma won't be available until Plasma 5.7,
so there's no hurry in getting this in (I'm targetting 16.08).

There is no configuration yet, because the current code in
Plasma Digital Clock applet does not implement it yet.
parent df6670f7
......@@ -57,6 +57,7 @@ find_package(KF5Config ${KF5_VERSION} CONFIG REQUIRED)
find_package(KF5DBusAddons ${KF5_VERSION} CONFIG REQUIRED)
find_package(KF5AkonadiNotes ${AKONADINOTES_LIB_VERSION} CONFIG REQUIRED)
find_package(KF5XmlGui ${KF5_VERSION} CONFIG REQUIRED)
find_package(KF5Declarative ${KF5_VERSION} CONFIG REQUIRED)
set(MAILCOMMN_LIB_VERSION "5.1.80")
set(GRAVATAR_LIB_VERSION "5.1.80")
......@@ -90,6 +91,7 @@ find_package(KF5MessageList ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED)
find_package(KF5CalendarSupport ${CALENDARSUPPORT_LIB_VERSION} CONFIG REQUIRED)
find_package(KF5EventViews ${EVENTVIEW_LIB_VERSION} CONFIG REQUIRED)
find_package(KF5Akonadi ${KDEPIMLIBS_LIB_VERSION} CONFIG REQUIRED)
find_package(KF5AkonadiCalendar ${KDEPIMLIBS_LIB_VERSION} CONFIG REQUIRED)
find_package(KF5Gravatar ${GRAVATAR_LIB_VERSION} CONFIG REQUIRED)
find_package(KF5Tnef ${KTNEF_LIB_VERSION} CONFIG REQUIRED)
......
......@@ -11,3 +11,4 @@ log_kmail_zoomtexteditorplugin kdepim-addons (kmail zoomtext editor plugins)
log_kmail_changecaseeditorplugin kdepim-addons (kmail changecase editor plugins)
log_adblockinterceptor kdepim-addons (adblock interceptor)
log_donottrackinterceptor kdepim-addons (do not track interceptor)
log_pimeventsplugin kdepim-addons (Plasma calendar plugin)
......@@ -3,6 +3,7 @@ add_subdirectory(messageviewerplugins)
add_subdirectory(messageviewerheaderplugins)
add_subdirectory(storageservices)
add_subdirectory(messageviewer)
add_subdirectory(plasma)
if (QTWEBENGINE_EXPERIMENTAL_OPTION)
add_subdirectory(messageviewerwebengineurlinterceptor)
......
add_subdirectory(pimeventsplugin)
set(pimeventsplugin_SRCS
pimeventsplugin.cpp
eventdatavisitor.cpp
)
ecm_qt_declare_logging_category(pimeventsplugin_SRCS
HEADER pimeventsplugin_debug.h
IDENTIFIER PIMEVENTSPLUGIN_LOG
CATEGORY_NAME log_pimeventsplugin
)
add_library(pimevents MODULE ${pimeventsplugin_SRCS})
target_link_libraries(pimevents
Qt5::Core
KF5::AkonadiCore
KF5::AkonadiCalendar
KF5::CalendarCore
KF5::CalendarEvents
)
install(TARGETS pimevents
DESTINATION ${PLUGIN_INSTALL_DIR}/plasmacalendarplugins
)
install(FILES PimEventsConfig.qml
DESTINATION ${PLUGIN_INSTALL_DIR}/plasmacalendarplugins/pimevents
)
/*
* Copyright 2016 Daniel Vrátil <dvratil@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 the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
import QtQuick 2.0
import QtQuick.Controls 1.2 as QtControls
import QtQuick.Layouts 1.0
import QtQuick.Dialogs 1.1
Item {
id: pimEventsConfig
width: parent.width
height: parent.height
// This is just for getting the column width
QtControls.CheckBox {
id: checkbox
visible: false
}
}
/*
* Copyright (C) 2016 Daniel Vrátil <dvratil@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
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*/
#include "eventdatavisitor.h"
#include "pimeventsplugin_debug.h"
#include <Akonadi/Calendar/ETMCalendar>
BaseEventDataVisitor::BaseEventDataVisitor(Akonadi::ETMCalendar *calendar,
const QDate &start, const QDate &end)
: mCalendar(calendar)
, mStart(start)
, mEnd(end)
{
}
BaseEventDataVisitor::~BaseEventDataVisitor()
{
}
bool BaseEventDataVisitor::act(const KCalCore::Incidence::Ptr &incidence)
{
return incidence->accept(*this, incidence);
}
bool BaseEventDataVisitor::act(const KCalCore::Event::List &events)
{
bool ok = false;
Q_FOREACH (const KCalCore::Event::Ptr &event, events) {
ok = event.staticCast<KCalCore::IncidenceBase>()->accept(*this, event) || ok;
}
return ok;
}
bool BaseEventDataVisitor::act(const KCalCore::Todo::List &todos)
{
bool ok = false;
Q_FOREACH (const KCalCore::Todo::Ptr &todo, todos) {
ok = todo.staticCast<KCalCore::IncidenceBase>()->accept(*this, todo) || ok;
}
return ok;
}
bool BaseEventDataVisitor::isInRange(const QDate &start, const QDate &end) const
{
if (!mStart.isValid() || !mEnd.isValid()) {
return true;
}
if (!end.isValid() && start >= mStart && start <= mEnd) {
return true;
} else if (start < mStart) {
return end >= mStart;
} else if (end > mEnd) {
return start <= mEnd;
} else {
return true;
}
}
QString BaseEventDataVisitor::generateUid(const KCalCore::Incidence::Ptr &incidence,
const KDateTime &recurrenceId) const
{
// Get a corresponding Akonadi Item: Akonadi ID is the only reliably unique
// and persistent identifier when dealing with incidences from multiple
// calendars
const Akonadi::Item item = mCalendar->item(incidence);
if (!item.isValid()) {
// Can this happen? What do we do now?!
return QString();
}
if (recurrenceId.isValid()) {
return QStringLiteral("Akonadi-%1-%2").arg(item.id())
.arg(recurrenceId.toString(QStringLiteral("%Y%m%dT%H%M%S%Z")));
} else {
return QStringLiteral("Akonadi-%1").arg(item.id());
}
}
EventDataVisitor::EventDataVisitor(Akonadi::ETMCalendar *calendar,
const QDate &start, const QDate &end)
: BaseEventDataVisitor(calendar, start , end)
{
}
EventDataVisitor::~EventDataVisitor()
{
}
QMultiHash<QDate, CalendarEvents::EventData> EventDataVisitor::results() const
{
return mResults;
}
bool EventDataVisitor::visit(const KCalCore::Event::Ptr &event)
{
CalendarEvents::EventData data = incidenceData(event);
data.setEventType(CalendarEvents::EventData::Event);
if (event->recurs()) {
return explodeIncidenceOccurences(data, event);
} else if (isInRange(event->dtStart().date(), event->dtEnd().date())) {
data.setStartDateTime(event->dtStart().dateTime());
data.setEndDateTime(event->dtEnd().dateTime());
mResults.insert(data.startDateTime().date(), data);
return true;
}
return false;
}
bool EventDataVisitor::visit(const KCalCore::Todo::Ptr &todo)
{
CalendarEvents::EventData data = incidenceData(todo);
data.setEventType(CalendarEvents::EventData::Todo);
if (todo->recurs()) {
return explodeIncidenceOccurences(data, todo);
} else if (isInRange(todo->dtStart().date(), todo->dtDue().date())) {
data.setStartDateTime(todo->dtStart().dateTime());
data.setEndDateTime(todo->dtDue().dateTime());
mResults.insert(data.startDateTime().date(), data);
return true;
}
return false;
}
CalendarEvents::EventData EventDataVisitor::incidenceData(const KCalCore::Incidence::Ptr &incidence) const
{
CalendarEvents::EventData data;
data.setTitle(incidence->summary());
data.setDescription(incidence->description());
data.setIsAllDay(incidence->allDay());
data.setIsMinor(false);
data.setUid(generateUid(incidence));
// TODO: Set calendar color
return data;
}
bool EventDataVisitor::explodeIncidenceOccurences(const CalendarEvents::EventData &ed,
const KCalCore::Incidence::Ptr &incidence)
{
Q_ASSERT(incidence->recurs());
const qint64 duration = ed.startDateTime().secsTo(ed.endDateTime());
KDateTime rec(mStart.addDays(-1), QTime(0, 0, 0));
rec = incidence->recurrence()->getNextDateTime(rec);
while (rec.isValid() && rec.date() <= mEnd) {
CalendarEvents::EventData copy = ed;
const QDateTime dt = rec.dateTime();
copy.setStartDateTime(dt);
// TODO: Is there a better way to find when an instance ends without
// going through the expensive lookup in Incidence::instance(uid, recurrenceId)?
copy.setEndDateTime(dt.addSecs(duration));
copy.setUid(generateUid(incidence, rec));
mResults.insert(dt.date(), copy);
rec = incidence->recurrence()->getNextDateTime(rec);
}
return true;
}
EventDataIdVisitor::EventDataIdVisitor(Akonadi::ETMCalendar *calendar, const QDate &start, const QDate &end)
: BaseEventDataVisitor(calendar, start, end)
{
}
QStringList EventDataIdVisitor::results() const
{
return mResults;
}
bool EventDataIdVisitor::visit(const KCalCore::Event::Ptr &event)
{
mResults.push_back(generateUid(event, event->recurrenceId()));
return true;
}
bool EventDataIdVisitor::visit(const KCalCore::Todo::Ptr &todo)
{
mResults.push_back(generateUid(todo, todo->recurrenceId()));
return true;
}
/*
* Copyright (C) 2016 Daniel Vrátil <dvratil@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
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*/
#ifndef EVENTDATAVISITOR_H
#define EVENTDATAVISITOR_H
#include <KCalCore/Visitor>
#include <QMultiHash>
#include <CalendarEvents/CalendarEventsPlugin>
namespace Akonadi
{
class ETMCalendar;
}
class BaseEventDataVisitor : public KCalCore::Visitor
{
public:
~BaseEventDataVisitor();
bool act(const KCalCore::Incidence::Ptr &incidence);
bool act(const KCalCore::Event::List &events);
bool act(const KCalCore::Todo::List &todos);
protected:
BaseEventDataVisitor(Akonadi::ETMCalendar *calendar, const QDate &start, const QDate &end);
QString generateUid(const KCalCore::Incidence::Ptr &incidence, const KDateTime &recurrenceId = KDateTime()) const;
bool isInRange(const QDate &start, const QDate &end) const;
protected:
Akonadi::ETMCalendar *mCalendar;
QDate mStart;
QDate mEnd;
};
class EventDataVisitor : public BaseEventDataVisitor
{
public:
EventDataVisitor(Akonadi::ETMCalendar *calendar, const QDate &start, const QDate &end);
~EventDataVisitor();
QMultiHash<QDate, CalendarEvents::EventData> results() const;
protected:
bool visit(const KCalCore::Event::Ptr &event) Q_DECL_OVERRIDE;
bool visit(const KCalCore::Todo::Ptr &todo) Q_DECL_OVERRIDE;
bool visit(const KCalCore::Journal::Ptr &) Q_DECL_OVERRIDE { return false; }
bool visit(const KCalCore::FreeBusy::Ptr &) Q_DECL_OVERRIDE { return false; }
bool explodeIncidenceOccurences(const CalendarEvents::EventData &ed,
const KCalCore::Incidence::Ptr &incidence);
private:
CalendarEvents::EventData incidenceData(const KCalCore::Incidence::Ptr &incidence) const;
QMultiHash<QDate, CalendarEvents::EventData> mResults;
};
class EventDataIdVisitor : public BaseEventDataVisitor
{
public:
explicit EventDataIdVisitor(Akonadi::ETMCalendar *calendar, const QDate &start, const QDate &end);
QStringList results() const;
protected:
bool visit(const KCalCore::Event::Ptr &event) Q_DECL_OVERRIDE;
bool visit(const KCalCore::Todo::Ptr &todo) Q_DECL_OVERRIDE;
bool visit(const KCalCore::Journal::Ptr &) Q_DECL_OVERRIDE { return false; }
bool visit(const KCalCore::FreeBusy::Ptr &) Q_DECL_OVERRIDE { return false; }
private:
QStringList mResults;
};
#endif
/*
* Copyright (C) 2016 Daniel Vrátil <dvratil@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
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*/
#include "pimeventsplugin.h"
#include "eventdatavisitor.h"
#include "pimeventsplugin_debug.h"
#include <Akonadi/Calendar/ETMCalendar>
#include <KCalCore/Visitor>
#define TRACE_RESULTS
QDebug operator<<(QDebug dbg, const CalendarEvents::EventData &data)
{
dbg.nospace() << data.title() << " (UID " << data.uid() << "), "
<< data.startDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss t"))
<< " - "
<< data.endDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss t"));
return dbg;
}
PimEventsPlugin::PimEventsPlugin(QObject* parent)
: CalendarEvents::CalendarEventsPlugin(parent)
, mCalendar(Q_NULLPTR)
{
qCDebug(PIMEVENTSPLUGIN_LOG) << "PIM Events Plugin activated";
mCalendar = new Akonadi::ETMCalendar(this);
// TODO: Filter out disabled calendars
// TOOD: Only retrieve PLD:HEAD once it's supported
mCalendar->setCollectionFilteringEnabled(false);
mCalendar->registerObserver(this);
}
PimEventsPlugin::~PimEventsPlugin()
{
}
void PimEventsPlugin::loadEventsForDateRange(const QDate &startDate, const QDate &endDate)
{
mStart = startDate;
mEnd = endDate;
qCDebug(PIMEVENTSPLUGIN_LOG) << "Requested range" << startDate << endDate;
{
EventDataVisitor visitor(mCalendar, startDate, endDate);
const KCalCore::Event::List events = mCalendar->events(startDate, endDate);
qCDebug(PIMEVENTSPLUGIN_LOG) << "\tFound" << events.count() << "events";
if (visitor.act(events)) {
qCDebug(PIMEVENTSPLUGIN_LOG) << "\tGenerated" << visitor.results().count() << "EventData";
#ifdef TRACE_RESULTS
Q_FOREACH (const auto &ed, visitor.results()) {
qCDebug(PIMEVENTSPLUGIN_LOG) << "\t" << ed;
}
#endif
Q_EMIT dataReady(visitor.results());
} else {
qCDebug(PIMEVENTSPLUGIN_LOG) << "\tGenerated 0 EventData";
}
}
{
EventDataVisitor visitor(mCalendar, startDate, endDate);
const KCalCore::Todo::List todos = mCalendar->todos(startDate, endDate);
qCDebug(PIMEVENTSPLUGIN_LOG) << "\tFound" << todos.count() << "todos";
if (visitor.act(todos)) {
qCDebug(PIMEVENTSPLUGIN_LOG) << "\tGenerated" << visitor.results().count() << "EventData";
#ifdef TRACE_RESULTS
Q_FOREACH (const auto &ed, visitor.results()) {
qCDebug(PIMEVENTSPLUGIN_LOG) << "\t" << ed;
}
#endif
Q_EMIT dataReady(visitor.results());
} else {
qCDebug(PIMEVENTSPLUGIN_LOG) << "\tGenerated 0 EventData";
}
}
}
void PimEventsPlugin::calendarIncidenceAdded(const KCalCore::Incidence::Ptr &incidence)
{
if (!mStart.isValid() || !mEnd.isValid()) {
// Don't bother with changes that happen before the applet starts populating data
return;
}
EventDataVisitor visitor(mCalendar, mStart, mEnd);
if (visitor.act(incidence)) {
qCDebug(PIMEVENTSPLUGIN_LOG) << "Incidence" << incidence->uid() << "added";
Q_EMIT dataReady(visitor.results());
}
}
void PimEventsPlugin::calendarIncidenceChanged(const KCalCore::Incidence::Ptr &incidence)
{
if (!mStart.isValid() || !mEnd.isValid()) {
return;
}
EventDataVisitor visitor(mCalendar, mStart, mEnd);
if (visitor.act(incidence)) {
Q_FOREACH (const auto &ed, visitor.results()) {
qCDebug(PIMEVENTSPLUGIN_LOG) << "EventData" << ed.uid() << "updated";
Q_EMIT eventModified(ed);
}
}
}
void PimEventsPlugin::calendarIncidenceAboutToBeDeleted(const KCalCore::Incidence::Ptr &incidence)
{
if (!mStart.isValid() || !mEnd.isValid()) {
return;
}
EventDataIdVisitor visitor(mCalendar, mStart, mEnd);
if (visitor.act(incidence)) {
Q_FOREACH (const QString &uid, visitor.results()) {
qCDebug(PIMEVENTSPLUGIN_LOG) << "EventData" << uid << "removed";
Q_EMIT eventRemoved(uid);
}
}
}
/*
* Copyright (C) 2016 Daniel Vrátil <dvratil@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
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
*/
#ifndef PIMEVENTSPLUGIN_H
#define PIMEVENTSPLUGIN_H
#include <CalendarEvents/CalendarEventsPlugin>
#include <KCalCore/Calendar>
namespace Akonadi
{
class ETMCalendar;
}
class PimEventsPlugin : public CalendarEvents::CalendarEventsPlugin
, public KCalCore::Calendar::CalendarObserver
{
Q_OBJECT
Q_INTERFACES(CalendarEvents::CalendarEventsPlugin)
Q_PLUGIN_METADATA(IID "org.kde.CalendarEventsPlugin" FILE "pimeventsplugin.json")
public:
explicit PimEventsPlugin(QObject *parent = Q_NULLPTR);
~PimEventsPlugin();
// CalendarEvents::CalendarEventsPlugin
void loadEventsForDateRange(const QDate &startDate, const QDate &endDate) Q_DECL_OVERRIDE;
// KCalCore::Calendar::CalendarObserver
void calendarIncidenceChanged(const KCalCore::Incidence::Ptr &incidence) Q_DECL_OVERRIDE;
void calendarIncidenceAdded(const KCalCore::Incidence::Ptr & incidence) Q_DECL_OVERRIDE;
// Handle removal before it really happens otherwise we would not be able
// to lookup corresponding Akonadi ID in ETMCalendar
void calendarIncidenceAboutToBeDeleted(const KCalCore::Incidence::Ptr &incidence) Q_DECL_OVERRIDE;
private:
Akonadi::ETMCalendar *mCalendar;
QDate mStart;
QDate mEnd;
};