File indexing completed on 2024-05-12 05:10:43
0001 /* 0002 SPDX-FileCopyrightText: 2022 Volker Krause <vkrause@kde.org> 0003 SPDX-FileCopyrightText: 2023 Daniel Vrátil <dvratil@kde.org> 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "collectioncalendar.h" 0008 #include "akonadicalendar_debug.h" 0009 #include "calendarbase_p.h" 0010 0011 #include <Akonadi/CalendarUtils> 0012 #include <Akonadi/EntityTreeModel> 0013 #include <Akonadi/ItemFetchJob> 0014 #include <Akonadi/ItemFetchScope> 0015 #include <Akonadi/Monitor> 0016 0017 using namespace KCalendarCore; 0018 0019 namespace Akonadi 0020 { 0021 0022 class CollectionCalendarPrivate : public CalendarBasePrivate 0023 { 0024 Q_OBJECT 0025 public: 0026 CollectionCalendarPrivate(EntityTreeModel *etm, CollectionCalendar *qq) 0027 : CalendarBasePrivate(qq) 0028 , m_etm(etm) 0029 , q(qq) 0030 { 0031 } 0032 0033 void setCollection(const Collection &col) 0034 { 0035 if (!col.isValid()) { 0036 return; 0037 } 0038 0039 Q_ASSERT(!m_collection.isValid()); 0040 m_collection = col; 0041 0042 if (m_monitor) { 0043 m_monitor->setCollectionMonitored(m_collection); 0044 } 0045 0046 init(); 0047 } 0048 0049 Collection m_collection; 0050 EntityTreeModel *m_etm = nullptr; 0051 Monitor *m_monitor = nullptr; 0052 bool m_populatedFromEtm = false; 0053 0054 QHash<Item::Id, Item> m_itemById; 0055 0056 private: 0057 void init() 0058 { 0059 if (!m_collection.isValid()) { 0060 return; 0061 } 0062 0063 if (!m_etm) { 0064 m_etm = createEtm(); 0065 } 0066 0067 connect(m_etm, &EntityTreeModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { 0068 if (!isMatchingCollection(parent)) { 0069 return; 0070 } 0071 0072 handleRowsInsertedUnchecked(parent, first, last); 0073 }); 0074 connect(m_etm, 0075 &EntityTreeModel::rowsAboutToBeMoved, 0076 this, 0077 [this](const QModelIndex &parent, int start, int end, const QModelIndex &newParent, int row) { 0078 Q_UNUSED(newParent); 0079 Q_UNUSED(row); 0080 // If rows are about to be moved from collection we monitor, it's like removal from our point of view. 0081 // Rows being moved into the collection we monitor is handled in rowsMoved signal handler. 0082 if (!isMatchingCollection(parent)) { 0083 return; 0084 } 0085 0086 handleRowsRemovedUnchecked(parent, start, end); 0087 }); 0088 connect(m_etm, &EntityTreeModel::rowsMoved, this, [this](const QModelIndex &parent, int start, int end, const QModelIndex &newParent, int row) { 0089 Q_UNUSED(parent); 0090 // If rows were moved into the collection we monitor, it's like they were added. 0091 // Rows being moved from the collection we monitor is handled in rowsAboutToBeRemoved signal handler. 0092 if (!isMatchingCollection(newParent)) { 0093 return; 0094 } 0095 0096 handleRowsInsertedUnchecked(newParent, row, row + (end - start)); 0097 }); 0098 connect(m_etm, &EntityTreeModel::rowsAboutToBeRemoved, this, [this](const QModelIndex &parent, int first, int last) { 0099 if (!isMatchingCollection(parent)) { 0100 return; 0101 } 0102 0103 handleRowsRemovedUnchecked(parent, first, last); 0104 }); 0105 connect(m_etm, &EntityTreeModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) { 0106 if (!isMatchingCollection(topLeft.parent())) { 0107 return; 0108 } 0109 0110 auto index = topLeft; 0111 for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { 0112 index = index.sibling(row, 0); 0113 const auto item = m_etm->data(index, EntityTreeModel::ItemRole).value<Item>(); 0114 if (item.isValid() || item.hasPayload<KCalendarCore::Incidence::Ptr>()) { 0115 updateItem(item); 0116 } 0117 } 0118 }); 0119 connect(m_etm, &EntityTreeModel::modelReset, this, [this]() { 0120 for (const auto &item : q->items()) { 0121 internalRemove(item); 0122 } 0123 m_populatedFromEtm = false; 0124 populateFromETM(); 0125 }); 0126 connect(m_etm, &EntityTreeModel::layoutChanged, this, &CollectionCalendarPrivate::populateFromETM); 0127 0128 populateFromETM(); 0129 } 0130 0131 void handleRowsRemovedUnchecked(const QModelIndex &parent, int first, int last) 0132 { 0133 for (int row = first; row <= last; ++row) { 0134 const auto index = m_etm->index(row, 0, parent); 0135 const auto item = m_etm->data(index, EntityTreeModel::ItemRole).value<Item>(); 0136 if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) { 0137 m_itemById.remove(item.id()); 0138 internalRemove(item); 0139 } 0140 } 0141 } 0142 0143 void handleRowsInsertedUnchecked(const QModelIndex &parent, int first, int last) 0144 { 0145 for (int row = first; row <= last; ++row) { 0146 const auto index = m_etm->index(row, 0, parent); 0147 const auto item = m_etm->data(index, EntityTreeModel::ItemRole).value<Item>(); 0148 if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) { 0149 m_itemById.insert(item.id(), item); 0150 internalInsert(item); 0151 } 0152 } 0153 } 0154 0155 bool isMatchingCollection(const QModelIndex &index) const 0156 { 0157 const auto colId = m_etm->data(index, EntityTreeModel::CollectionIdRole).toLongLong(); 0158 return colId == m_collection.id(); 0159 } 0160 0161 void updateItem(const Item &item) 0162 { 0163 Incidence::Ptr newIncidence = CalendarUtils::incidence(item); 0164 newIncidence->setCustomProperty("VOLATILE", "AKONADI-ID", QString::number(item.id())); 0165 IncidenceBase::Ptr existingIncidence = q->incidence(newIncidence->uid(), newIncidence->recurrenceId()); 0166 0167 auto oldItem = m_itemById.value(item.id()); 0168 0169 if (existingIncidence) { 0170 auto updatedItem = item; 0171 updatedItem.setPayload(existingIncidence.staticCast<KCalendarCore::Incidence>()); 0172 m_itemById.insert(item.id(), updatedItem); 0173 0174 (*existingIncidence.data()) = *(newIncidence.data()); 0175 } else { 0176 m_itemById.insert(item.id(), item); 0177 handleUidChange(oldItem, item, newIncidence->instanceIdentifier()); 0178 } 0179 } 0180 0181 void populateFromETM() 0182 { 0183 if (m_populatedFromEtm) { 0184 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - already populated"; 0185 return; 0186 } 0187 m_populatedFromEtm = true; 0188 0189 if (!m_etm->isCollectionTreeFetched()) { 0190 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - collection tree not fetched"; 0191 return; 0192 } 0193 0194 if (!m_etm->isCollectionPopulated(m_collection.id())) { 0195 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar not populating from ETM - target collection not populated yet"; 0196 return; 0197 } 0198 0199 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar populating from ETM"; 0200 0201 q->setIsLoading(true); 0202 const auto colIdx = EntityTreeModel::modelIndexForCollection(m_etm, m_collection); 0203 Q_ASSERT(colIdx.isValid()); 0204 if (!colIdx.isValid()) { 0205 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar failed to populate from ETM - couldn't find model index for our Collection" 0206 << m_collection.id(); 0207 return; 0208 } 0209 0210 q->startBatchAdding(); 0211 auto idx = m_etm->index(0, 0, colIdx); 0212 std::size_t itemCount = 0; 0213 while (idx.isValid()) { 0214 const auto item = m_etm->data(idx, EntityTreeModel::ItemRole).value<Item>(); 0215 if (item.isValid() && item.hasPayload<KCalendarCore::Incidence::Ptr>()) { 0216 internalInsert(item); 0217 ++itemCount; 0218 } 0219 idx = idx.siblingAtRow(idx.row() + 1); 0220 } 0221 q->endBatchAdding(); 0222 0223 q->setIsLoading(false); 0224 0225 qCDebug(AKONADICALENDAR_LOG) << "CollectionCalendar for Collection" << m_collection.id() << "populated from ETM with" << itemCount << "incidences"; 0226 } 0227 0228 EntityTreeModel *createEtm() 0229 { 0230 m_monitor = new Monitor(this); 0231 m_monitor->setCollectionMonitored(m_collection); 0232 m_monitor->itemFetchScope().fetchFullPayload(); 0233 m_monitor->itemFetchScope().setCacheOnly(true); 0234 m_monitor->itemFetchScope().setAncestorRetrieval(ItemFetchScope::AncestorRetrieval::Parent); 0235 for (const auto &mt : KCalendarCore::Incidence::mimeTypes()) { 0236 m_monitor->setMimeTypeMonitored(mt, true); 0237 } 0238 0239 return new EntityTreeModel(m_monitor, this); 0240 } 0241 0242 CollectionCalendar *const q; 0243 }; 0244 0245 } // namespace Akonadi 0246 0247 using namespace Akonadi; 0248 0249 CollectionCalendar::CollectionCalendar(const Akonadi::Collection &col, QObject *parent) 0250 : Akonadi::CalendarBase(new CollectionCalendarPrivate(nullptr, this), parent) 0251 { 0252 setCollection(col); 0253 0254 incidenceChanger()->setDefaultCollection(col); 0255 incidenceChanger()->setGroupwareCommunication(false); 0256 incidenceChanger()->setDestinationPolicy(Akonadi::IncidenceChanger::DestinationPolicyNeverAsk); 0257 } 0258 0259 CollectionCalendar::CollectionCalendar(Akonadi::EntityTreeModel *etm, const Akonadi::Collection &col, QObject *parent) 0260 : Akonadi::CalendarBase(new CollectionCalendarPrivate(etm, this), parent) 0261 { 0262 setCollection(col); 0263 } 0264 0265 CollectionCalendar::~CollectionCalendar() = default; 0266 0267 Akonadi::Collection CollectionCalendar::collection() const 0268 { 0269 Q_D(const CollectionCalendar); 0270 return d->m_collection; 0271 } 0272 0273 void CollectionCalendar::setCollection(const Akonadi::Collection &c) 0274 { 0275 Q_D(CollectionCalendar); 0276 0277 if (c.id() == d->m_collection.id()) { 0278 return; 0279 } 0280 0281 Q_ASSERT(!d->m_collection.isValid()); 0282 if (d->m_collection.isValid()) { 0283 qCWarning(AKONADICALENDAR_LOG) << "Cannot change collection of CollectionCalendar at runtime yet, sorry."; 0284 return; 0285 } 0286 0287 setName(Akonadi::CalendarUtils::displayName(d->m_etm, c)); 0288 setAccessMode((c.rights() & (Akonadi::Collection::CanCreateItem | Akonadi::Collection::CanChangeItem)) ? KCalendarCore::ReadWrite 0289 : KCalendarCore::ReadOnly); 0290 d->setCollection(c); 0291 } 0292 0293 Akonadi::EntityTreeModel *CollectionCalendar::model() const 0294 { 0295 Q_D(const CollectionCalendar); 0296 return d->m_etm; 0297 } 0298 0299 bool CollectionCalendar::addEvent(const KCalendarCore::Event::Ptr &event) 0300 { 0301 Q_D(CollectionCalendar); 0302 0303 if (d->m_collection.contentMimeTypes().contains(event->mimeType()) || d->m_collection.contentMimeTypes().contains(QLatin1StringView("text/calendar"))) { 0304 return CalendarBase::addEvent(event); 0305 } 0306 return false; 0307 } 0308 0309 bool CollectionCalendar::addTodo(const KCalendarCore::Todo::Ptr &todo) 0310 { 0311 Q_D(CollectionCalendar); 0312 0313 if (d->m_collection.contentMimeTypes().contains(todo->mimeType()) || d->m_collection.contentMimeTypes().contains(QLatin1StringView("text/calendar"))) { 0314 return CalendarBase::addTodo(todo); 0315 } 0316 return false; 0317 } 0318 0319 bool CollectionCalendar::addJournal(const KCalendarCore::Journal::Ptr &journal) 0320 { 0321 Q_D(CollectionCalendar); 0322 0323 if (d->m_collection.contentMimeTypes().contains(journal->mimeType()) || d->m_collection.contentMimeTypes().contains(QLatin1StringView("text/calendar"))) { 0324 return CalendarBase::addJournal(journal); 0325 } 0326 return false; 0327 } 0328 0329 bool CollectionCalendar::hasRight(Akonadi::Collection::Right right) const 0330 { 0331 Q_D(const CollectionCalendar); 0332 const auto fullCollection = Akonadi::EntityTreeModel::updatedCollection(d->m_etm, d->m_collection); 0333 Q_ASSERT(fullCollection.isValid()); 0334 if (!fullCollection.isValid()) { 0335 return false; 0336 } 0337 0338 return (fullCollection.rights() & right) == right; 0339 } 0340 0341 #include "collectioncalendar.moc" 0342 #include "moc_collectioncalendar.cpp"