diff --git a/framework/src/domain/multidayeventmodel.cpp b/framework/src/domain/multidayeventmodel.cpp index c531887d..4bc11942 100644 --- a/framework/src/domain/multidayeventmodel.cpp +++ b/framework/src/domain/multidayeventmodel.cpp @@ -1,176 +1,224 @@ /* 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 "multidayeventmodel.h" #include #include #include #include enum Roles { Events = EventModel::LastRole, WeekStartDate }; MultiDayEventModel::MultiDayEventModel(QObject *parent) : QAbstractItemModel(parent) { } QModelIndex MultiDayEventModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) { return {}; } if (!parent.isValid()) { return createIndex(row, column); } return {}; } QModelIndex MultiDayEventModel::parent(const QModelIndex &) const { return {}; } int MultiDayEventModel::rowCount(const QModelIndex &parent) const { //Number of weeks if (!parent.isValid() && mSourceModel) { return qMax(mSourceModel->length() / 7, 1); } return 0; } int MultiDayEventModel::columnCount(const QModelIndex &) const { return 1; } QVariant MultiDayEventModel::data(const QModelIndex &idx, int role) const { if (!hasIndex(idx.row(), idx.column())) { return {}; } if (!mSourceModel) { return {}; } const auto rowStart = mSourceModel->start().addDays(idx.row() * 7); switch (role) { case WeekStartDate: return rowStart; case Events: { + /* + * Layout the lines: + * We first sort all occurences so we get all-day first (sorted by duration), + * and then the rest sorted by start-date. + * + * The line grouping algorithm then always picks the first event, + * and tries to add more to the same line. + * + * We never mix all-day and non-all day, and otherwise try to fit as much as possible + * on the same line. Same day time-order should be preserved because of the sorting. + */ const auto rowEnd = rowStart.addDays(7); - auto getStart = [&] (const QDate &start) { + auto getStart = [&rowStart] (const QDate &start) { return qMax(rowStart.daysTo(start), 0ll); }; - auto getDuration = [&] (const QDate &start, const QDate &end) { + auto getDuration = [] (const QDate &start, const QDate &end) { return qMax(start.daysTo(end), 1ll); }; - QMultiMap sorted; - //TODO: - //All-day first, sorted by duration - //then sort by start time + QList sorted; + sorted.reserve(mSourceModel->rowCount()); for (int row = 0; row < mSourceModel->rowCount(); row++) { const auto srcIdx = mSourceModel->index(row, 0, {}); const auto start = srcIdx.data(EventModel::StartTime).toDateTime().date(); const auto end = srcIdx.data(EventModel::EndTime).toDateTime().date(); - //Filter if not within this row - //FIXME: avoid iterating over all events for every week + //Skip events not part of the week if (end < rowStart || start > rowEnd) { continue; } - sorted.insert(getDuration(start, end), srcIdx); + sorted.append(srcIdx); } + qSort(sorted.begin(), sorted.end(), [&] (const QModelIndex &left, const QModelIndex &right) { + //All-day first, sorted by duration (in the hope that we can fit multiple on the same line) + const auto leftAllDay = left.data(EventModel::AllDay).toBool(); + const auto rightAllDay = right.data(EventModel::AllDay).toBool(); + if (leftAllDay && !rightAllDay) { + return true; + } + if (!leftAllDay && rightAllDay) { + return false; + } + if (leftAllDay && rightAllDay) { + const auto leftDuration = getDuration(left.data(EventModel::StartTime).toDateTime().date(), left.data(EventModel::EndTime).toDateTime().date()); + const auto rightDuration = getDuration(right.data(EventModel::StartTime).toDateTime().date(), right.data(EventModel::EndTime).toDateTime().date()); + return leftDuration < rightDuration; + } + //The rest sorted by start date + return left.data(EventModel::StartTime).toDateTime() < right.data(EventModel::StartTime).toDateTime(); + }); auto result = QVariantList{}; - auto currentLine = QVariantList{}; - int lastStart = -1; - int lastDuration = 0; - for (const auto &srcIdx : sorted) { + while (!sorted.isEmpty()) { + const auto srcIdx = sorted.takeFirst(); const auto start = getStart(srcIdx.data(EventModel::StartTime).toDateTime().date()); const auto duration = qMin(getDuration(srcIdx.data(EventModel::StartTime).toDateTime().date(), srcIdx.data(EventModel::EndTime).toDateTime().date()), mPeriodLength - start); const auto end = start + duration; - currentLine.append(QVariantMap{ - {"text", srcIdx.data(EventModel::Summary)}, - {"description", srcIdx.data(EventModel::Description)}, - {"starts", start}, - {"duration", duration}, - {"color", srcIdx.data(EventModel::Color)}, - }); - - if (lastStart >= 0) { + qWarning() << "start " << srcIdx.data(EventModel::StartTime).toDateTime() << duration; + auto currentLine = QVariantList{}; + + auto addToLine = [¤tLine] (const QModelIndex &idx, int start, int duration) { + currentLine.append(QVariantMap{ + {"text", idx.data(EventModel::Summary)}, + {"description", idx.data(EventModel::Description)}, + {"starts", start}, + {"duration", duration}, + {"color", idx.data(EventModel::Color)}, + }); + }; + + //Add first event of line + addToLine(srcIdx, start, duration); + const bool allDayLine = srcIdx.data(EventModel::AllDay).toBool(); + + //Fill line with events that fit + int lastStart = 0; + int lastDuration = 0; + auto doesIntersect = [&] (int start, int end) { const auto lastEnd = lastStart + lastDuration; - - //Does intersect - if (((start >= lastStart) && (start <= lastEnd)) || - ((end >= lastStart) && (end <= lastStart)) || - ((start <= lastStart) && (end >= lastEnd))) { - result.append(QVariant::fromValue(currentLine)); - // qDebug() << "Found intersection " << currentLine; - currentLine = {}; + if (((start <= lastStart) && (end >= lastStart)) || + ((start < lastEnd) && (end > lastStart))) { + // qWarning() << "Found intersection " << start << end; + return true; } + return false; + }; + + + for (auto it = sorted.begin(); it != sorted.end();) { + const auto idx = *it; + const auto start = getStart(idx.data(EventModel::StartTime).toDateTime().date()); + const auto duration = qMin(getDuration(idx.data(EventModel::StartTime).toDateTime().date(), idx.data(EventModel::EndTime).toDateTime().date()), mPeriodLength - start); + const auto end = start + duration; + //Avoid mixing all-day and other events + if (allDayLine && !idx.data(EventModel::AllDay).toBool()) { + break; + } + if (doesIntersect(start, end)) { + it++; + continue; + } + addToLine(idx, start, duration); + lastStart = start; + lastDuration = duration; + it = sorted.erase(it); } - lastStart = start; - lastDuration = duration; - } - if (!currentLine.isEmpty()) { + // qWarning() << "Appending line " << currentLine; result.append(QVariant::fromValue(currentLine)); } - - // qDebug() << "Found events " << result; return result; } default: Q_ASSERT(false); return {}; } } void MultiDayEventModel::setModel(EventModel *model) { beginResetModel(); mSourceModel = model; auto resetModel = [this] { beginResetModel(); endResetModel(); }; QObject::connect(model, &QAbstractItemModel::dataChanged, this, resetModel); QObject::connect(model, &QAbstractItemModel::layoutChanged, this, resetModel); QObject::connect(model, &QAbstractItemModel::modelReset, this, resetModel); QObject::connect(model, &QAbstractItemModel::rowsInserted, this, resetModel); QObject::connect(model, &QAbstractItemModel::rowsMoved, this, resetModel); QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, resetModel); endResetModel(); } QHash MultiDayEventModel::roleNames() const { return { {Events, "events"}, {WeekStartDate, "weekStartDate"} }; } diff --git a/framework/src/tests/eventmodeltest.cpp b/framework/src/tests/eventmodeltest.cpp index 4433bcec..3aafb50b 100644 --- a/framework/src/tests/eventmodeltest.cpp +++ b/framework/src/tests/eventmodeltest.cpp @@ -1,112 +1,145 @@ #include #include #include #include #include #include #include #include #include "eventmodel.h" #include "multidayeventmodel.h" #include "perioddayeventmodel.h" class EventModelTest : public QObject { Q_OBJECT private slots: void initTestCase() { Sink::Test::initTest(); } void testEventModel() { Sink::ApplicationDomain::DummyResource::create("account1"); using namespace Sink::ApplicationDomain; auto account = ApplicationDomainType::createEntity(); Sink::Store::create(account).exec().waitForFinished(); auto resource = Sink::ApplicationDomain::DummyResource::create(account.identifier()); Sink::Store::create(resource).exec().waitForFinished(); auto calendar1 = ApplicationDomainType::createEntity(resource.identifier()); Sink::Store::create(calendar1).exec().waitForFinished(); const QDateTime start{{2018, 04, 17}, {6, 0, 0}}; { auto event1 = ApplicationDomainType::createEntity(resource.identifier()); auto calcoreEvent = QSharedPointer::create(); calcoreEvent->setUid("event1"); calcoreEvent->setSummary("summary1"); calcoreEvent->setDescription("description"); calcoreEvent->setDtStart(start); calcoreEvent->setDuration(3600); calcoreEvent->setAllDay(false); event1.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); event1.setCalendar(calendar1); Sink::Store::create(event1).exec().waitForFinished(); } { auto event2 = ApplicationDomainType::createEntity(resource.identifier()); auto calcoreEvent = QSharedPointer::create(); calcoreEvent->setUid("event2"); calcoreEvent->setSummary("summary2"); calcoreEvent->setDescription("description"); calcoreEvent->setDtStart(start.addDays(1)); calcoreEvent->setDuration(3600); calcoreEvent->setAllDay(false); calcoreEvent->recurrence()->setDaily(1); event2.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); event2.setCalendar(calendar1); Sink::Store::create(event2).exec().waitForFinished(); } + { + auto event3 = ApplicationDomainType::createEntity(resource.identifier()); + auto calcoreEvent = QSharedPointer::create(); + calcoreEvent->setUid("event3"); + calcoreEvent->setSummary("summary3"); + calcoreEvent->setDtStart(start.addDays(1)); + calcoreEvent->setDuration(3600); + calcoreEvent->setAllDay(false); + event3.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); + event3.setCalendar(calendar1); + Sink::Store::create(event3).exec().waitForFinished(); + } + { + auto event4 = ApplicationDomainType::createEntity(resource.identifier()); + auto calcoreEvent = QSharedPointer::create(); + calcoreEvent->setUid("event3"); + calcoreEvent->setSummary("summary3"); + calcoreEvent->setDtStart(start.addDays(2)); + calcoreEvent->setAllDay(true); + event4.setIcal(KCalCore::ICalFormat().toICalString(calcoreEvent).toUtf8()); + event4.setCalendar(calendar1); + Sink::Store::create(event4).exec().waitForFinished(); + } Sink::ResourceControl::flushMessageQueue(resource.identifier()).exec().waitForFinished(); { + const int expectedNumberOfOccurreces = 9; + const int numberOfDays = 7; EventModel model; model.setStart(start.date()); - model.setLength(7); + model.setLength(numberOfDays); model.setCalendarFilter({calendar1.identifier()}); - QTRY_COMPARE(model.rowCount({}), 7); + QTRY_COMPARE(model.rowCount({}), expectedNumberOfOccurreces); + + auto countEvents = [&] (const QVariantList &lines) { + int count = 0; + for (const auto &line : lines) { + count += line.toList().size(); + } + return count; + }; //Check the multidayevent model { MultiDayEventModel multiDayModel; multiDayModel.setModel(&model); QTRY_COMPARE(multiDayModel.rowCount({}), 1); - //All except the first from the recurring event - //FIXME This test fails sometimes - // QTRY_COMPARE(multiDayModel.index(0, 0, {}).data(multiDayModel.roleNames().key("events")).value().size(), 6); + QTRY_COMPARE(countEvents(multiDayModel.index(0, 0, {}).data(multiDayModel.roleNames().key("events")).value()), expectedNumberOfOccurreces); + //Count lines + QTRY_COMPARE(multiDayModel.index(0, 0, {}).data(multiDayModel.roleNames().key("events")).value().size(), 3); } { PeriodDayEventModel multiDayModel; multiDayModel.setModel(&model); - QTRY_COMPARE(multiDayModel.rowCount({}), 7); + QTRY_COMPARE(multiDayModel.rowCount({}), numberOfDays); { const auto events = multiDayModel.index(0, 0, {}).data(multiDayModel.roleNames().key("events")).value(); QCOMPARE(events.size(), 1); } { const auto events = multiDayModel.index(1, 0, {}).data(multiDayModel.roleNames().key("events")).value(); - QCOMPARE(events.size(), 1); + QCOMPARE(events.size(), 2); } { const auto events = multiDayModel.index(2, 0, {}).data(multiDayModel.roleNames().key("events")).value(); QCOMPARE(events.size(), 1); } } model.setCalendarFilter({}); QTRY_COMPARE(model.rowCount({}), 0); } } }; QTEST_MAIN(EventModelTest) #include "eventmodeltest.moc"