File indexing completed on 2025-01-19 04:52:00

0001 /*
0002     Copyright (c) 2018 Michael Bohlender <michael.bohlender@kdemail.net>
0003     Copyright (c) 2018 Christian Mollekopf <mollekopf@kolabsys.com>
0004     Copyright (c) 2018 RĂ©mi Nicole <minijackson@riseup.net>
0005 
0006     This library is free software; you can redistribute it and/or modify it
0007     under the terms of the GNU Library General Public License as published by
0008     the Free Software Foundation; either version 2 of the License, or (at your
0009     option) any later version.
0010 
0011     This library is distributed in the hope that it will be useful, but WITHOUT
0012     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0013     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
0014     License for more details.
0015 
0016     You should have received a copy of the GNU Library General Public License
0017     along with this library; see the file COPYING.LIB.  If not, write to the
0018     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0019     02110-1301, USA.
0020 */
0021 
0022 #include "multidayeventmodel.h"
0023 
0024 #include <sink/log.h>
0025 #include <sink/query.h>
0026 #include <sink/store.h>
0027 
0028 #include <eventoccurrencemodel.h>
0029 
0030 enum Roles {
0031     Events = EventOccurrenceModel::LastRole,
0032     WeekStartDate
0033 };
0034 
0035 MultiDayEventModel::MultiDayEventModel(QObject *parent)
0036     : QAbstractItemModel(parent),
0037     mUpdateFromSourceDebouncer{100,[this] { this->reset(); }, this}
0038 {
0039 }
0040 
0041 QModelIndex MultiDayEventModel::index(int row, int column, const QModelIndex &parent) const
0042 {
0043     if (!hasIndex(row, column, parent)) {
0044         return {};
0045     }
0046 
0047     if (!parent.isValid()) {
0048         return createIndex(row, column);
0049     }
0050     return {};
0051 }
0052 
0053 QModelIndex MultiDayEventModel::parent(const QModelIndex &) const
0054 {
0055     return {};
0056 }
0057 
0058 int MultiDayEventModel::rowCount(const QModelIndex &parent) const
0059 {
0060     //Number of weeks
0061     if (!parent.isValid() && mSourceModel) {
0062         return qMax(mSourceModel->length() / 7, 1);
0063     }
0064     return 0;
0065 }
0066 
0067 int MultiDayEventModel::columnCount(const QModelIndex &) const
0068 {
0069     return 1;
0070 }
0071 
0072 
0073 static long long getDuration(const QDate &start, const QDate &end)
0074 {
0075     return qMax(start.daysTo(end) + 1, 1ll);
0076 }
0077 
0078 // We first sort all occurences so we get all-day first (sorted by duration),
0079 // and then the rest sorted by start-date.
0080 QList<QModelIndex> MultiDayEventModel::sortedEventsFromSourceModel(const QDate &rowStart) const
0081 {
0082     const auto rowEnd = rowStart.addDays(7);
0083     QList<QModelIndex> sorted;
0084     sorted.reserve(mSourceModel->rowCount());
0085     for (int row = 0; row < mSourceModel->rowCount(); row++) {
0086         const auto srcIdx = mSourceModel->index(row, 0, {});
0087         const auto start = srcIdx.data(EventOccurrenceModel::StartTime).toDateTime().date();
0088         const auto end = srcIdx.data(EventOccurrenceModel::EndTime).toDateTime().date();
0089         //Skip events not part of the week
0090         if (end < rowStart || start > rowEnd) {
0091             // qWarning() << "Skipping because not part of this week";
0092             continue;
0093         }
0094         // qWarning() << "found " << srcIdx.data(EventOccurrenceModel::StartTime).toDateTime() << srcIdx.data(EventOccurrenceModel::Summary).toString();
0095         sorted.append(srcIdx);
0096     }
0097     std::sort(sorted.begin(), sorted.end(), [&] (const QModelIndex &left, const QModelIndex &right) {
0098         //All-day first, sorted by duration (in the hope that we can fit multiple on the same line)
0099         const auto leftAllDay = left.data(EventOccurrenceModel::AllDay).toBool();
0100         const auto rightAllDay = right.data(EventOccurrenceModel::AllDay).toBool();
0101         if (leftAllDay && !rightAllDay) {
0102             return true;
0103         }
0104         if (!leftAllDay && rightAllDay) {
0105             return false;
0106         }
0107         if (leftAllDay && rightAllDay) {
0108             const auto leftDuration = getDuration(left.data(EventOccurrenceModel::StartTime).toDateTime().date(), left.data(EventOccurrenceModel::EndTime).toDateTime().date());
0109             const auto rightDuration = getDuration(right.data(EventOccurrenceModel::StartTime).toDateTime().date(), right.data(EventOccurrenceModel::EndTime).toDateTime().date());
0110             return leftDuration < rightDuration;
0111         }
0112         //The rest sorted by start date
0113         return left.data(EventOccurrenceModel::StartTime).toDateTime() < right.data(EventOccurrenceModel::StartTime).toDateTime();
0114     });
0115     return sorted;
0116 }
0117 
0118 /*
0119 * Layout the lines:
0120 *
0121 * The line grouping algorithm then always picks the first event,
0122 * and tries to add more to the same line.
0123 *
0124 * We never mix all-day and non-all day, and otherwise try to fit as much as possible
0125 * on the same line. Same day time-order should be preserved because of the sorting.
0126 */
0127 QVariantList MultiDayEventModel::layoutLines(const QDate &rowStart) const
0128 {
0129     auto getStart = [&rowStart] (const QDate &start) {
0130         return qMax(rowStart.daysTo(start), 0ll);
0131     };
0132 
0133     auto getStartOfLineDate = [&rowStart] (const QDate &start) {
0134         if (rowStart < start) {
0135             return start;
0136         }
0137         return rowStart;
0138     };
0139 
0140     QList<QModelIndex> sorted = sortedEventsFromSourceModel(rowStart);
0141 
0142     // for (const auto &srcIdx : sorted) {
0143     //     qWarning() << "sorted " << srcIdx.data(EventOccurrenceModel::StartTime).toDateTime() << srcIdx.data(EventOccurrenceModel::Summary).toString() << srcIdx.data(EventOccurrenceModel::AllDay).toBool();
0144     // }
0145 
0146     auto result = QVariantList{};
0147     while (!sorted.isEmpty()) {
0148         const auto srcIdx = sorted.takeFirst();
0149         const auto startDate = srcIdx.data(EventOccurrenceModel::StartTime).toDateTime();
0150         const auto start = getStart(startDate.date());
0151         const auto duration = qMin(getDuration(getStartOfLineDate(startDate.date()), srcIdx.data(EventOccurrenceModel::EndTime).toDateTime().date()), mPeriodLength - start);
0152         // qWarning() << "First of line " << srcIdx.data(EventOccurrenceModel::StartTime).toDateTime() << duration << srcIdx.data(EventOccurrenceModel::Summary).toString();
0153         auto currentLine = QVariantList{};
0154 
0155         auto addToLine = [&currentLine] (const QModelIndex &idx, int start, int duration) {
0156             currentLine.append(QVariantMap{
0157                 {"text", idx.data(EventOccurrenceModel::Summary)},
0158                 {"description", idx.data(EventOccurrenceModel::Description)},
0159                 {"starts", start},
0160                 {"duration", duration},
0161                 {"color", idx.data(EventOccurrenceModel::Color)},
0162                 {"eventOccurrence", idx.data(EventOccurrenceModel::EventOccurrence)}
0163             });
0164         };
0165 
0166         //Add first event of line
0167         addToLine(srcIdx, start, duration);
0168         const bool allDayLine = srcIdx.data(EventOccurrenceModel::AllDay).toBool();
0169 
0170         //Fill line with events that fit
0171         int lastStart = start;
0172         int lastDuration = duration;
0173         auto doesIntersect = [&] (int start, int end) {
0174             const auto lastEnd = lastStart + lastDuration;
0175             if (((start <= lastStart) && (end >= lastStart)) ||
0176                 ((start < lastEnd) && (end > lastStart))) {
0177                 // qWarning() << "Found intersection " << start << end;
0178                 return true;
0179             }
0180             return false;
0181         };
0182 
0183         for (auto it = sorted.begin(); it != sorted.end();) {
0184             const auto idx = *it;
0185             const auto start = getStart(idx.data(EventOccurrenceModel::StartTime).toDateTime().date());
0186             const auto duration = qMin(getDuration(idx.data(EventOccurrenceModel::StartTime).toDateTime().date(), idx.data(EventOccurrenceModel::EndTime).toDateTime().date()), mPeriodLength - start);
0187             const auto end = start + duration;
0188 
0189             // qWarning() << "Checking " << idx.data(EventOccurrenceModel::StartTime).toDateTime() << duration << idx.data(EventOccurrenceModel::Summary).toString();
0190             //Avoid mixing all-day and other events
0191             if (allDayLine && !idx.data(EventOccurrenceModel::AllDay).toBool()) {
0192                 break;
0193             }
0194 
0195             if (doesIntersect(start, end)) {
0196                 it++;
0197             } else {
0198                 addToLine(idx, start, duration);
0199                 lastStart = start;
0200                 lastDuration = duration;
0201                 it = sorted.erase(it);
0202             }
0203         }
0204         // qWarning() << "Appending line " << currentLine;
0205         result.append(QVariant::fromValue(currentLine));
0206     }
0207     return result;
0208 }
0209 
0210 QVariant MultiDayEventModel::data(const QModelIndex &idx, int role) const
0211 {
0212     if (!hasIndex(idx.row(), idx.column())) {
0213         return {};
0214     }
0215     if (!mSourceModel) {
0216         return {};
0217     }
0218     const auto rowStart = mSourceModel->start().addDays(idx.row() * 7);
0219     switch (role) {
0220         case WeekStartDate:
0221             return rowStart;
0222         case Events:
0223             return layoutLines(rowStart);
0224         default:
0225             Q_ASSERT(false);
0226             return {};
0227     }
0228 }
0229 
0230 void MultiDayEventModel::reset()
0231 {
0232     beginResetModel();
0233     endResetModel();
0234 }
0235 
0236 void MultiDayEventModel::setModel(EventOccurrenceModel *model)
0237 {
0238     beginResetModel();
0239     mSourceModel = model;
0240     auto resetModel = [this] {
0241         mUpdateFromSourceDebouncer.trigger();
0242     };
0243     QObject::connect(model, &QAbstractItemModel::dataChanged, this, resetModel);
0244     QObject::connect(model, &QAbstractItemModel::layoutChanged, this, resetModel);
0245     QObject::connect(model, &QAbstractItemModel::modelReset, this, resetModel);
0246     QObject::connect(model, &QAbstractItemModel::rowsInserted, this, resetModel);
0247     QObject::connect(model, &QAbstractItemModel::rowsMoved, this, resetModel);
0248     QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, resetModel);
0249     endResetModel();
0250 }
0251 
0252 QHash<int, QByteArray> MultiDayEventModel::roleNames() const
0253 {
0254     return {
0255         {Events, "events"},
0256         {WeekStartDate, "weekStartDate"}
0257     };
0258 }