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 = [¤tLine] (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 }