diff --git a/framework/src/CMakeLists.txt b/framework/src/CMakeLists.txt --- a/framework/src/CMakeLists.txt +++ b/framework/src/CMakeLists.txt @@ -1,6 +1,7 @@ find_package(Qt5 COMPONENTS REQUIRED Core Concurrent Quick Qml WebEngineWidgets Test WebEngine Gui) find_package(KF5Mime 4.87.0 CONFIG REQUIRED) +find_package(KF5CalendarCore CONFIG REQUIRED) find_package(Sink 0.6.0 CONFIG REQUIRED) find_package(KAsync CONFIG REQUIRED) find_package(QGpgme CONFIG REQUIRED) @@ -16,6 +17,7 @@ settings/settings.cpp domain/maillistmodel.cpp domain/folderlistmodel.cpp + domain/perioddayeventmodel.cpp domain/composercontroller.cpp domain/modeltest.cpp domain/retriever.cpp diff --git a/framework/src/domain/perioddayeventmodel.h b/framework/src/domain/perioddayeventmodel.h new file mode 100644 --- /dev/null +++ b/framework/src/domain/perioddayeventmodel.h @@ -0,0 +1,129 @@ +/* + Copyright (c) 2018 Michael Bohlender + Copyright (c) 2018 Christian Mollekopf + Copyright (c) 2018 Rémi Nicole + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library 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 Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#pragma once + +#include + +#include +#include +#include +#include + +#include + +// Facility used to get a restricted period into a Sink model comprised of +// events, partitioned according to the day the events take place. +// +// Model Format +// ============ +// +// Day 0 +// |--- Event 0 starting at `periodStart + 0d` +// |--- Event 1 starting at `periodStart + 0d` +// '--- Event 2 starting at `periodStart + 0d` +// Day 1 +// '--- Event 0 starting at `periodStart + 1d` +// Day 2 +// Day 3 +// |--- Event 0 starting at `periodStart + 3d` +// '--- Event 1 starting at `periodStart + 3d` +// Day 4 +// ⋮ +// +// Implementation notes +// ==================== +// +// On the model side +// ----------------- +// +// Columns are never used. +// +// Top-level items just contains the ".events" attribute, and their rows +// correspond to their offset compared to the start of the period (in number of +// days). In that case the internalId contains DAY_ID. +// +// Direct children are events, and their rows corresponds to their index in +// their partition. In that case no internalId / internalPointer is used. +// +// Internally: +// ----------- +// +// On construction and on dataChanged, all events are processed and partitioned +// in partitionedEvents: +// +// QVector< QList > +// ~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// | | +// | '--- List of event pointers for that day +// '--- Partition / day +// +class PeriodDayEventModel : public QAbstractItemModel +{ + Q_OBJECT + + Q_PROPERTY(QVariant start READ periodStart WRITE setPeriodStart) + Q_PROPERTY(int length READ periodLength WRITE setPeriodLength) + +public: + using Event = Sink::ApplicationDomain::Event; + + enum Roles + { + Events = Qt::UserRole + 1, + Summary, + Description, + StartTime, + Duration, + }; + Q_ENUM(Roles); + PeriodDayEventModel(QObject *parent = nullptr); + ~PeriodDayEventModel() = default; + + QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; + QModelIndex parent(const QModelIndex &index) const override; + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + + QHash roleNames() const override; + + QDate periodStart() const; + void setPeriodStart(const QDate &); + void setPeriodStart(const QVariant &); + int periodLength() const; + void setPeriodLength(int); + +private: + void partitionData(); + + int bucketOf(const QDate &candidate) const; + + QDate mPeriodStart; + int mPeriodLength = 7; + + QSharedPointer eventModel; + QVector>> partitionedEvents; + + static const constexpr quintptr DAY_ID = std::numeric_limits::max(); +}; diff --git a/framework/src/domain/perioddayeventmodel.cpp b/framework/src/domain/perioddayeventmodel.cpp new file mode 100644 --- /dev/null +++ b/framework/src/domain/perioddayeventmodel.cpp @@ -0,0 +1,256 @@ +/* + Copyright (c) 2018 Michael Bohlender + Copyright (c) 2018 Christian Mollekopf + Copyright (c) 2018 Rémi Nicole + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library 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 Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#include "perioddayeventmodel.h" + +#include +#include +#include + +#include +#include +#include + +PeriodDayEventModel::PeriodDayEventModel(QObject *parent) + : QAbstractItemModel(parent), partitionedEvents(7) +{ + Sink::Query query; + query.setFlags(Sink::Query::LiveQuery); + query.request(); + query.request(); + query.request(); + query.request(); + + eventModel = Sink::Store::loadModel(query); + + QObject::connect(eventModel.data(), &QAbstractItemModel::dataChanged, this, &PeriodDayEventModel::partitionData); + QObject::connect(eventModel.data(), &QAbstractItemModel::layoutChanged, this, &PeriodDayEventModel::partitionData); + QObject::connect(eventModel.data(), &QAbstractItemModel::modelReset, this, &PeriodDayEventModel::partitionData); + QObject::connect(eventModel.data(), &QAbstractItemModel::rowsInserted, this, &PeriodDayEventModel::partitionData); + QObject::connect(eventModel.data(), &QAbstractItemModel::rowsMoved, this, &PeriodDayEventModel::partitionData); + QObject::connect(eventModel.data(), &QAbstractItemModel::rowsRemoved, this, &PeriodDayEventModel::partitionData); + + partitionData(); +} + +void PeriodDayEventModel::partitionData() +{ + SinkLog() << "Partitioning event data"; + + beginResetModel(); + + partitionedEvents = QVector>>(mPeriodLength); + + for (int i = 0; i < eventModel->rowCount(); ++i) { + auto event = eventModel->index(i, 0).data(Sink::Store::DomainObjectRole).value(); + QDate eventDate = event->getStartTime().date(); + + if (!eventDate.isValid()) { + SinkWarning() << "Invalid date in the eventModel, ignoring..."; + continue; + } + + int bucket = bucketOf(eventDate); + + if (bucket >= 0) { + SinkTrace() << "Adding event:" << event->getSummary() << "in bucket #" << bucket; + partitionedEvents[bucket].append(event); + } + } + + endResetModel(); +} + +int PeriodDayEventModel::bucketOf(const QDate &candidate) const +{ + int bucket = mPeriodStart.daysTo(candidate); + if (bucket >= mPeriodLength || bucket < 0) { + return -1; + } + + return bucket; +} + +QModelIndex PeriodDayEventModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) { + return {}; + } + + if (!parent.isValid()) { + // Asking for a day + + if (!(0 <= row && row < mPeriodLength)) { + return {}; + } + + return createIndex(row, column, DAY_ID); + } + + // Asking for an Event + auto day = static_cast(parent.row()); + + Q_ASSERT(0 <= day && day <= mPeriodLength); + if (row >= partitionedEvents[day].size()) { + return {}; + } + + return createIndex(row, column, day); +} + +QModelIndex PeriodDayEventModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) { + return {}; + } + + if (index.internalId() == DAY_ID) { + return {}; + } + + auto day = index.internalId(); + + return this->index(day, 0); +} + +int PeriodDayEventModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return mPeriodLength; + } + + auto day = parent.row(); + + return partitionedEvents[day].size(); +} + +int PeriodDayEventModel::columnCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return 1; + } + + return eventModel->columnCount(); +} + +QVariant PeriodDayEventModel::data(const QModelIndex &id, int role) const +{ + if (id.internalId() == DAY_ID) { + auto day = id.row(); + + SinkTrace() << "Fetching data for day" << day << "with role" + << QMetaEnum::fromType().valueToKey(role); + + switch (role) { + case Qt::DisplayRole: + return mPeriodStart.addDays(day).toString(); + case Events: { + auto result = QVariantList{}; + + for (int i = 0; i < partitionedEvents[day].size(); ++i) { + auto eventId = index(i, 0, id); + SinkTrace() << "Appending event:" << data(eventId, Summary); + + auto startTime = data(eventId, StartTime).toDateTime().time(); + + result.append(QVariantMap{ + {"text", data(eventId, Summary)}, + {"description", data(eventId, Description)}, + {"starts", startTime.hour() + startTime.minute() / 60.}, + {"duration", data(eventId, Duration)}, + {"color", "#134bab"}, + {"indention", 0}, + }); + } + + return result; + } + default: + SinkWarning() << "Unknown role for day:" << QMetaEnum::fromType().valueToKey(role); + return {}; + } + } else { + auto day = id.internalId(); + SinkTrace() << "Fetching data for event on day" << day << "with role" + << QMetaEnum::fromType().valueToKey(role); + auto event = partitionedEvents[day].at(id.row()); + + switch (role) { + case Summary: + return event->getSummary(); + case Description: + return event->getDescription(); + case StartTime: + return event->getStartTime(); + case Duration: { + auto start = event->getStartTime(); + auto end = event->getEndTime(); + return start.secsTo(end) / 3600; + } + default: + SinkWarning() << "Unknown role for event:" << QMetaEnum::fromType().valueToKey(role); + return {}; + } + } +} + +QHash PeriodDayEventModel::roleNames() const +{ + return { + {Events, "events"}, + {Summary, "summary"}, + {Description, "description"}, + {StartTime, "starts"}, + {Duration, "duration"}, + }; +} + +QDate PeriodDayEventModel::periodStart() const +{ + return mPeriodStart; +} + +void PeriodDayEventModel::setPeriodStart(const QDate &start) +{ + if (!start.isValid()) { + SinkWarning() << "Passed an invalid starting date in setPeriodStart, ignoring..."; + return; + } + + mPeriodStart = start; + partitionData(); +} + +void PeriodDayEventModel::setPeriodStart(const QVariant &start) +{ + setPeriodStart(start.toDate()); +} + +int PeriodDayEventModel::periodLength() const +{ + return mPeriodLength; +} + +void PeriodDayEventModel::setPeriodLength(int length) +{ + mPeriodLength = length; + partitionData(); +} diff --git a/framework/src/frameworkplugin.cpp b/framework/src/frameworkplugin.cpp --- a/framework/src/frameworkplugin.cpp +++ b/framework/src/frameworkplugin.cpp @@ -22,6 +22,7 @@ #include "domain/maillistmodel.h" #include "domain/folderlistmodel.h" +#include "domain/perioddayeventmodel.h" #include "domain/composercontroller.h" #include "domain/mime/messageparser.h" #include "domain/retriever.h" @@ -120,6 +121,7 @@ { qmlRegisterType(uri, 1, 0, "FolderListModel"); qmlRegisterType(uri, 1, 0, "MailListModel"); + qmlRegisterType(uri, 1, 0, "PeriodDayEventModel"); qmlRegisterType(uri, 1, 0, "ComposerController"); qmlRegisterType(uri, 1, 0, "ControllerAction"); qmlRegisterType(uri, 1, 0, "MessageParser"); diff --git a/views/calendar/qml/WeekEvents.qml b/views/calendar/qml/WeekEvents.qml --- a/views/calendar/qml/WeekEvents.qml +++ b/views/calendar/qml/WeekEvents.qml @@ -1,93 +1,8 @@ import QtQuick 2.7 -ListModel { - ListElement { - events: [ - ListElement { - color: "#af1a6a" - starts: 1 - duration: 4 - text: "Meeting" - indention: 0 - }, - ListElement { - color: "#134bab" - starts: 9 - duration: 5 - text: "Sport" - indention: 0 - } - ] - } - ListElement { - events: [ - ListElement { - color: "#134bab" - starts: 9 - duration: 5 - text: "Sport" - indention: 0 - } - ] - } - ListElement { - events: [] - } - ListElement { - events: [ - ListElement { - color: "#af1a6a" - starts: 1 - duration: 4 - indention: 0 - text: "Meeting" - } - ] - } - ListElement { - events: [ - ListElement { - color: "#134bab" - starts: 3 - duration: 5 - indention: 0 - text: "Meeting" - }, - ListElement { - color: "#af1a6a" - starts: 4 - duration: 7 - indention: 1 - text: "Meeting2" - } - ] - } - ListElement { - events: [ - ListElement { - color: "#134bab" - starts: 8 - duration: 5 - indention: 0 - text: "Meeting" - }, - ListElement { - color: "#af1a6a" - starts: 8 - duration: 4 - indention: 1 - text: "Meeting2" - }, - ListElement { - color: "#af1a6a" - starts: 9 - duration: 7 - indention: 2 - text: "Meeting2" - } - ] - } - ListElement { - events: [] - } +import org.kube.framework 1.0 as Kube + +Kube.PeriodDayEventModel { + start: "2018-04-09" + length: 7 } diff --git a/views/calendar/qml/WeekView.qml b/views/calendar/qml/WeekView.qml --- a/views/calendar/qml/WeekView.qml +++ b/views/calendar/qml/WeekView.qml @@ -212,12 +212,12 @@ right: parent.right rightMargin: Kube.Units.smallSpacing } - width: Kube.Units.gridUnit * 7 - Kube.Units.smallSpacing * 2 - Kube.Units.gridUnit * model.indention - height: Kube.Units.gridUnit * model.duration - y: Kube.Units.gridUnit * model.starts - x: Kube.Units.gridUnit * model.indention + width: Kube.Units.gridUnit * 7 - Kube.Units.smallSpacing * 2 - Kube.Units.gridUnit * model.modelData.indention + height: Kube.Units.gridUnit * model.modelData.duration + y: Kube.Units.gridUnit * model.modelData.starts + x: Kube.Units.gridUnit * model.modelData.indention - color: model.color + color: model.modelData.color border.width: 1 border.color: Kube.Colors.viewBackgroundColor @@ -226,7 +226,7 @@ left: parent.left leftMargin: Kube.Units.smallSpacing } - text: model.text + text: model.modelData.text color: Kube.Colors.highlightedTextColor }