File indexing completed on 2024-06-23 04:42:37
0001 // SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com> 0002 // SPDX-License-Identifier: LGPL-2.1-or-later 0003 0004 #include "todosortfilterproxymodel.h" 0005 #include "../filter.h" 0006 0007 TodoSortFilterProxyModel::TodoSortFilterProxyModel(QObject *parent) 0008 : QSortFilterProxyModel(parent) 0009 { 0010 const QString todoMimeType = QStringLiteral("application/x-vnd.akonadi.calendar.todo"); 0011 m_todoTreeModel.reset(new Akonadi::IncidenceTreeModel(QStringList() << todoMimeType, this)); 0012 0013 m_baseTodoModel.reset(new Akonadi::TodoModel(this)); 0014 m_baseTodoModel->setSourceModel(m_todoTreeModel.data()); 0015 setSourceModel(m_baseTodoModel.data()); 0016 0017 setDynamicSortFilter(true); 0018 setSortCaseSensitivity(Qt::CaseInsensitive); 0019 setFilterCaseSensitivity(Qt::CaseInsensitive); 0020 0021 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0022 KConfigGroup rColorsConfig(config, "Resources Colors"); 0023 m_colorWatcher = KConfigWatcher::create(config); 0024 QObject::connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &TodoSortFilterProxyModel::loadColors); 0025 0026 loadColors(); 0027 0028 m_dateRefreshTimer.setInterval(m_dateRefreshTimerInterval); 0029 m_dateRefreshTimer.callOnTimeout(this, &TodoSortFilterProxyModel::updateDateLabels); 0030 m_dateRefreshTimer.start(); 0031 } 0032 0033 int TodoSortFilterProxyModel::columnCount(const QModelIndex &) const 0034 { 0035 return 1; 0036 } 0037 0038 QHash<int, QByteArray> TodoSortFilterProxyModel::roleNames() const 0039 { 0040 QHash<int, QByteArray> roleNames = QSortFilterProxyModel::roleNames(); 0041 roleNames[Akonadi::TodoModel::SummaryRole] = "text"; 0042 roleNames[Roles::StartTimeRole] = "startTime"; 0043 roleNames[Roles::EndTimeRole] = "endTime"; 0044 roleNames[Roles::DisplayDueDateRole] = "displayDueDate"; 0045 roleNames[Roles::LocationRole] = "location"; 0046 roleNames[Roles::AllDayRole] = "allDay"; 0047 roleNames[Roles::ColorRole] = "color"; 0048 roleNames[Roles::CompletedRole] = "todoCompleted"; 0049 roleNames[Roles::PriorityRole] = "priority"; 0050 roleNames[Roles::CollectionIdRole] = "collectionId"; 0051 roleNames[Roles::DurationStringRole] = "durationString"; 0052 roleNames[Roles::RecursRole] = "recurs"; 0053 roleNames[Roles::IsOverdueRole] = "isOverdue"; 0054 roleNames[Roles::IncidenceIdRole] = "incidenceId"; 0055 roleNames[Roles::IncidenceTypeRole] = "incidenceType"; 0056 roleNames[Roles::IncidenceTypeStrRole] = "incidenceTypeStr"; 0057 roleNames[Roles::IncidenceTypeIconRole] = "incidenceTypeIcon"; 0058 roleNames[Roles::IncidencePtrRole] = "incidencePtr"; 0059 roleNames[Roles::TagsRole] = "tags"; 0060 roleNames[Roles::ItemRole] = "item"; 0061 roleNames[Roles::CategoriesRole] = "todoCategories"; // Simply 'categories' causes issues 0062 roleNames[Roles::CategoriesDisplayRole] = "categoriesDisplay"; 0063 roleNames[Roles::TreeDepthRole] = "treeDepth"; 0064 roleNames[Roles::TopMostParentDueDateRole] = "topMostParentDueDate"; 0065 roleNames[Roles::TopMostParentSummaryRole] = "topMostParentSummary"; 0066 roleNames[Roles::TopMostParentPriorityRole] = "topMostParentPriority"; 0067 0068 return roleNames; 0069 } 0070 0071 QVariant TodoSortFilterProxyModel::data(const QModelIndex &index, int role) const 0072 { 0073 if (!index.isValid() || m_calendar.isNull()) { 0074 return {}; 0075 } 0076 0077 const QModelIndex sourceIndex = mapToSource(index.sibling(index.row(), 0)); 0078 if (!sourceIndex.isValid()) { 0079 return {}; 0080 } 0081 Q_ASSERT(sourceIndex.isValid()); 0082 0083 auto todoItem = sourceIndex.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>(); 0084 0085 if (!todoItem.isValid()) { 0086 return {}; 0087 } 0088 0089 auto collectionId = todoItem.parentCollection().id(); 0090 auto todoPtr = Akonadi::CalendarUtils::todo(todoItem); 0091 0092 if (!todoPtr) { 0093 return {}; 0094 } 0095 0096 if (role == Roles::StartTimeRole) { 0097 return todoPtr->dtStart(); 0098 } else if (role == Roles::EndTimeRole) { 0099 return todoPtr->dtDue(); 0100 } else if (role == Roles::DisplayDueDateRole) { 0101 return todoDueDateDisplayString(todoPtr, DisplayDateTimeAndIfOverdue); 0102 } else if (role == Roles::LocationRole) { 0103 return todoPtr->location(); 0104 } else if (role == Roles::AllDayRole) { 0105 return todoPtr->allDay(); 0106 } else if (role == Roles::ColorRole) { 0107 QColor nullcolor; 0108 return m_colors.contains(QString::number(collectionId)) ? m_colors[QString::number(collectionId)] : nullcolor; 0109 } else if (role == Roles::CompletedRole) { 0110 return todoPtr->isCompleted(); 0111 } else if (role == Roles::PriorityRole) { 0112 return todoPtr->priority(); 0113 } else if (role == Roles::CollectionIdRole) { 0114 return collectionId; 0115 } else if (role == DurationStringRole) { 0116 const auto duration = KCalendarCore::Duration(todoPtr->dtStart(), todoPtr->dtDue()); 0117 0118 if (todoPtr->allDay() && !todoPtr->dtStart().isValid()) { 0119 return m_format.formatSpelloutDuration(24 * 60 * 60 * 1000); // format milliseconds in 1 day 0120 } else if (!todoPtr->dtStart().isValid() || duration.asSeconds() == 0) { 0121 return QString(); 0122 } 0123 0124 return m_format.formatSpelloutDuration(duration.asSeconds() * 1000); 0125 } else if (role == Roles::RecursRole) { 0126 return todoPtr->recurs(); 0127 } else if (role == Roles::IsOverdueRole) { 0128 return todoPtr->isOverdue(); 0129 } else if (role == Roles::IncidenceIdRole) { 0130 return todoPtr->uid(); 0131 } else if (role == Roles::IncidenceTypeRole) { 0132 return todoPtr->type(); 0133 } else if (role == Roles::IncidenceTypeStrRole) { 0134 return todoPtr->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n(todoPtr->typeStr().constData()); 0135 } else if (role == Roles::IncidenceTypeIconRole) { 0136 return todoPtr->iconName(); 0137 } else if (role == Roles::IncidencePtrRole) { 0138 return QVariant::fromValue(Akonadi::CalendarUtils::incidence(todoItem)); 0139 } else if (role == Roles::TagsRole) { 0140 return QVariant::fromValue(todoItem.tags()); 0141 } else if (role == Roles::ItemRole) { 0142 return QVariant::fromValue(todoItem); 0143 } else if (role == Roles::CategoriesRole) { 0144 return todoPtr->categories(); 0145 } else if (role == Roles::CategoriesDisplayRole) { 0146 return todoPtr->categories().join(i18nc("List separator", ", ")); 0147 } else if (role == Roles::TreeDepthRole || role == TopMostParentSummaryRole || role == TopMostParentDueDateRole || role == TopMostParentPriorityRole) { 0148 int depth = 0; 0149 auto idx = index; 0150 while (idx.parent().isValid()) { 0151 idx = idx.parent(); 0152 depth++; 0153 } 0154 0155 auto todo = idx.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0156 0157 switch (role) { 0158 case Roles::TreeDepthRole: 0159 return depth; 0160 case TopMostParentSummaryRole: 0161 return todo->summary(); 0162 case TopMostParentDueDateRole: { 0163 if (!todo->hasDueDate()) { 0164 return i18n("No set date"); 0165 } 0166 0167 if (todo->isOverdue()) { 0168 return i18n("Overdue"); 0169 } 0170 0171 const auto dateInCurrentTZ = todo->dtDue().toLocalTime().date(); 0172 const auto isToday = dateInCurrentTZ == QDate::currentDate(); 0173 0174 return isToday ? i18n("Today") : todoDueDateDisplayString(todo, DisplayDateOnly); 0175 } 0176 case TopMostParentPriorityRole: 0177 return todo->priority(); 0178 } 0179 } 0180 return QSortFilterProxyModel::data(index, role); 0181 } 0182 0183 QString TodoSortFilterProxyModel::todoDueDateDisplayString(const KCalendarCore::Todo::Ptr todo, 0184 const TodoSortFilterProxyModel::DueDateDisplayFormat format) const 0185 { 0186 if (!todo || !todo->hasDueDate()) { 0187 return {}; 0188 } 0189 0190 const auto systemLocale = QLocale::system(); 0191 const auto includeTime = !todo->allDay() && format != DisplayDateOnly; 0192 const auto includeOverdue = todo->isOverdue() && format == DisplayDateTimeAndIfOverdue; 0193 0194 const auto todoDateTimeDue = todo->dtDue().toLocalTime(); 0195 const auto todoDateDue = todoDateTimeDue.date(); 0196 const auto todoTimeDueString = 0197 includeTime ? i18nc("Please retain space", " at %1", systemLocale.toString(todoDateTimeDue.time(), QLocale::NarrowFormat)) : QStringLiteral(" "); 0198 const auto todoOverdueString = includeOverdue ? i18nc("Please retain parenthesis and space", " (overdue)") : QString(); 0199 0200 const auto currentDate = QDate::currentDate(); 0201 const auto dateFormat = todoDateDue.year() == currentDate.year() ? QStringLiteral("dddd dd MMMM") : QStringLiteral("dddd dd MMMM yyyy"); 0202 0203 static constexpr char translationExplainer[] = 0204 "No spaces -- the (optional) %1 string, which includes the time, includes this space" 0205 " as does the %2 string which is the overdue string (also optional!)"; 0206 0207 if (currentDate == todoDateDue) { 0208 return i18nc(translationExplainer, "Today%1%2", todoTimeDueString, todoOverdueString); 0209 } else if (currentDate.daysTo(todoDateDue) == 1) { 0210 return i18nc(translationExplainer, "Tomorrow%1%2", todoTimeDueString, todoOverdueString); 0211 } else if (currentDate.daysTo(todoDateDue) == -1) { 0212 return i18nc(translationExplainer, "Yesterday%1%2", todoTimeDueString, todoOverdueString); 0213 } 0214 0215 const auto dateDueString = systemLocale.toString(todoDateDue, dateFormat); 0216 return dateDueString + todoTimeDueString + todoOverdueString; 0217 } 0218 0219 void TodoSortFilterProxyModel::updateDateLabels() 0220 { 0221 if (rowCount() == 0 || !sourceModel()) { 0222 return; 0223 } 0224 0225 emitDateDataChanged({}); 0226 sortTodoModel(); 0227 m_lastDateRefreshDate = QDate::currentDate(); 0228 } 0229 0230 void TodoSortFilterProxyModel::emitDateDataChanged(const QModelIndex &idx) 0231 { 0232 const auto idxRowCount = rowCount(idx); 0233 const auto srcModel = sourceModel(); 0234 0235 if (idxRowCount == 0) { 0236 return; 0237 } 0238 0239 const auto bottomRow = idxRowCount - 1; 0240 const auto currentDate = QDate::currentDate(); 0241 const auto currentDateTime = QDateTime::currentDateTime(); 0242 0243 const auto iterateOverChildren = [this, &idx, &srcModel, ¤tDate, ¤tDateTime](const int row) { 0244 const auto childIdx = index(row, 0, idx); 0245 0246 const auto todo = childIdx.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0247 const auto isOverdue = todo->isOverdue(); 0248 const auto dtDue = todo->dtDue(); 0249 const auto isRecentlyOverdue = isOverdue && currentDateTime.msecsTo(dtDue) >= -m_dateRefreshTimerInterval; 0250 0251 if (isRecentlyOverdue || m_lastDateRefreshDate != currentDate) { 0252 Q_EMIT dataChanged(childIdx, childIdx, {DisplayDueDateRole, TopMostParentDueDateRole}); 0253 0254 // For the proxy model to re-sort items into their correct section we also need to emit a 0255 // dataChanged() signal for the column we are sorting by in the source model 0256 const auto srcChildIdx = mapToSource(childIdx).siblingAtColumn(Akonadi::TodoModel::DueDateColumn); 0257 Q_EMIT srcModel->dataChanged(srcChildIdx, srcChildIdx, {Akonadi::TodoModel::DueDateRole}); 0258 } 0259 0260 // We recursively do the same for children 0261 emitDateDataChanged(childIdx); 0262 }; 0263 0264 // This is a workaround for weird sorting behaviour. If one of the items changes because it becomes 0265 // overdue, for example, the way in which we emit dataChanged() signals of the sourceModel will 0266 // dictate how the model gets sorted. 0267 // 0268 // Example: In a case where the sort is ascending (i.e. overdue at top), a 0 to rowCount() -1 sort 0269 // will move the overdue item up by only one row due to how the QSortFilterProxyModel uses lessThan(). 0270 // If we go the opposite way then the QSFPM calls lessThan() on the overdue item more than once, moving 0271 // it upwards. 0272 0273 if (m_sortAscending) { 0274 for (auto i = bottomRow; i >= 0; --i) { 0275 iterateOverChildren(i); 0276 } 0277 } else { 0278 for (auto i = 0; i < idxRowCount; ++i) { 0279 iterateOverChildren(i); 0280 } 0281 } 0282 } 0283 0284 bool TodoSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &sourceParent) const 0285 { 0286 if (filterAcceptsRowCheck(row, sourceParent)) { 0287 return true; 0288 } 0289 0290 // Accept if any parent is accepted itself, and if we are the model for the incomplete tasks view, only do this if the config says to show all 0291 // of a tasks' incomplete subtasks. By default we include all of a tasks' subtasks, regardless of if they are complete or not, as long as the parent 0292 // passes the filter check. If this is not the case, we only include subtasks that pass the filter themselves. 0293 0294 if ((m_showCompletedSubtodosInIncomplete && m_showCompleted == ShowComplete::ShowIncompleteOnly) || m_showCompleted != ShowComplete::ShowIncompleteOnly) { 0295 QModelIndex parent = sourceParent; 0296 while (parent.isValid()) { 0297 if (filterAcceptsRowCheck(parent.row(), parent.parent())) 0298 return true; 0299 parent = parent.parent(); 0300 } 0301 } 0302 0303 // Accept if any child is accepted itself 0304 return hasAcceptedChildren(row, sourceParent); 0305 } 0306 0307 bool TodoSortFilterProxyModel::filterAcceptsRowCheck(int row, const QModelIndex &sourceParent) const 0308 { 0309 const QModelIndex sourceIndex = sourceModel()->index(row, 0, sourceParent); 0310 Q_ASSERT(sourceIndex.isValid()); 0311 0312 if (m_filterObject == nullptr) { 0313 return QSortFilterProxyModel::filterAcceptsRow(row, sourceParent); 0314 } 0315 0316 bool acceptRow = true; 0317 0318 if (m_filterObject->collectionId() > -1) { 0319 const auto collectionId = sourceIndex.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>().parentCollection().id(); 0320 acceptRow = acceptRow && collectionId == m_filterObject->collectionId(); 0321 } 0322 0323 switch (m_showCompleted) { 0324 case ShowComplete::ShowCompleteOnly: 0325 acceptRow = acceptRow && sourceIndex.data(Akonadi::TodoModel::PercentRole).toInt() == 100; 0326 break; 0327 case ShowComplete::ShowIncompleteOnly: 0328 acceptRow = acceptRow && sourceIndex.data(Akonadi::TodoModel::PercentRole).toInt() < 100; 0329 case ShowComplete::ShowAll: 0330 default: 0331 break; 0332 } 0333 0334 if (!m_filterObject->tags().isEmpty()) { 0335 const auto tags = m_filterObject->tags(); 0336 bool containsTag = false; 0337 for (const auto &tag : tags) { 0338 const auto todoPtr = sourceIndex.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0339 if (todoPtr->categories().contains(tag)) { 0340 containsTag = true; 0341 break; 0342 } 0343 } 0344 acceptRow = acceptRow && containsTag; 0345 } 0346 0347 return acceptRow ? QSortFilterProxyModel::filterAcceptsRow(row, sourceParent) : acceptRow; 0348 } 0349 0350 bool TodoSortFilterProxyModel::hasAcceptedChildren(int row, const QModelIndex &sourceParent) const 0351 { 0352 QModelIndex index = sourceModel()->index(row, 0, sourceParent); 0353 if (!index.isValid()) { 0354 return false; 0355 } 0356 0357 int childCount = index.model()->rowCount(index); 0358 if (childCount == 0) 0359 return false; 0360 0361 for (int i = 0; i < childCount; ++i) { 0362 if (filterAcceptsRowCheck(i, index)) 0363 return true; 0364 0365 if (hasAcceptedChildren(i, index)) 0366 return true; 0367 } 0368 0369 return false; 0370 } 0371 0372 Akonadi::ETMCalendar::Ptr TodoSortFilterProxyModel::calendar() const 0373 { 0374 return m_calendar; 0375 } 0376 0377 void TodoSortFilterProxyModel::setCalendar(Akonadi::ETMCalendar::Ptr &calendar) 0378 { 0379 // No need to manually emit beginResetModel(), source model does it for us 0380 m_calendar = calendar; 0381 m_todoTreeModel->setSourceModel(calendar->model()); 0382 m_baseTodoModel->setCalendar(m_calendar); 0383 Q_EMIT calendarChanged(); 0384 } 0385 0386 Akonadi::IncidenceChanger *TodoSortFilterProxyModel::incidenceChanger() const 0387 { 0388 return m_lastSetChanger; 0389 } 0390 0391 void TodoSortFilterProxyModel::setIncidenceChanger(Akonadi::IncidenceChanger *changer) 0392 { 0393 m_baseTodoModel->setIncidenceChanger(changer); 0394 m_lastSetChanger = changer; 0395 0396 Q_EMIT incidenceChangerChanged(); 0397 } 0398 0399 void TodoSortFilterProxyModel::setColorCache(const QHash<QString, QColor> colorCache) 0400 { 0401 m_colors = colorCache; 0402 } 0403 0404 void TodoSortFilterProxyModel::loadColors() 0405 { 0406 Q_EMIT layoutAboutToBeChanged(); 0407 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0408 KConfigGroup rColorsConfig(config, "Resources Colors"); 0409 const QStringList colorKeyList = rColorsConfig.keyList(); 0410 0411 for (const QString &key : colorKeyList) { 0412 QColor color = rColorsConfig.readEntry(key, QColor("blue")); 0413 m_colors[key] = color; 0414 } 0415 Q_EMIT layoutChanged(); 0416 } 0417 0418 int TodoSortFilterProxyModel::showCompleted() const 0419 { 0420 return m_showCompleted; 0421 } 0422 0423 void TodoSortFilterProxyModel::setShowCompleted(int showCompleted) 0424 { 0425 Q_EMIT layoutAboutToBeChanged(); 0426 m_showCompleted = showCompleted; 0427 m_showCompletedStore = showCompleted; // For when we search 0428 invalidateFilter(); 0429 Q_EMIT showCompletedChanged(); 0430 Q_EMIT layoutChanged(); 0431 0432 sortTodoModel(); 0433 } 0434 0435 Filter *TodoSortFilterProxyModel::filterObject() const 0436 { 0437 return m_filterObject; 0438 } 0439 0440 void TodoSortFilterProxyModel::setFilterObject(Filter *filterObject) 0441 { 0442 if (m_filterObject == filterObject) { 0443 return; 0444 } 0445 0446 if (m_filterObject) { 0447 disconnect(m_filterObject, nullptr, this, nullptr); 0448 } 0449 0450 Q_EMIT filterObjectAboutToChange(); 0451 Q_EMIT layoutAboutToBeChanged(); 0452 m_filterObject = filterObject; 0453 Q_EMIT filterObjectChanged(); 0454 0455 const auto nameFilter = m_filterObject->name(); 0456 const auto handleFilterNameChange = [this] { 0457 Q_EMIT filterObjectAboutToChange(); 0458 setFilterFixedString(m_filterObject->name()); 0459 Q_EMIT layoutChanged(); 0460 Q_EMIT filterObjectChanged(); 0461 }; 0462 const auto handleFilterObjectChange = [this] { 0463 Q_EMIT filterObjectAboutToChange(); 0464 invalidateFilter(); 0465 Q_EMIT layoutChanged(); 0466 Q_EMIT filterObjectChanged(); 0467 }; 0468 0469 connect(m_filterObject, &Filter::nameChanged, this, handleFilterNameChange); 0470 connect(m_filterObject, &Filter::tagsChanged, this, handleFilterObjectChange); 0471 connect(m_filterObject, &Filter::collectionIdChanged, this, handleFilterObjectChange); 0472 0473 if (!nameFilter.isEmpty()) { 0474 setFilterFixedString(nameFilter); 0475 } 0476 0477 invalidateFilter(); 0478 0479 Q_EMIT layoutChanged(); 0480 0481 sortTodoModel(); 0482 } 0483 0484 void TodoSortFilterProxyModel::sortTodoModel() 0485 { 0486 const auto order = m_sortAscending ? Qt::AscendingOrder : Qt::DescendingOrder; 0487 QSortFilterProxyModel::sort(m_sortColumn, order); 0488 } 0489 0490 void TodoSortFilterProxyModel::filterTodoName(const QString &name, const int showCompleted) 0491 { 0492 Q_EMIT layoutAboutToBeChanged(); 0493 setFilterFixedString(name); 0494 if (!name.isEmpty()) { 0495 m_showCompleted = showCompleted; 0496 } else { 0497 setShowCompleted(m_showCompletedStore); 0498 } 0499 invalidateFilter(); 0500 Q_EMIT layoutChanged(); 0501 0502 sortTodoModel(); 0503 } 0504 0505 int TodoSortFilterProxyModel::compareStartDates(const QModelIndex &left, const QModelIndex &right) const 0506 { 0507 Q_ASSERT(left.column() == Akonadi::TodoModel::StartDateColumn); 0508 Q_ASSERT(right.column() == Akonadi::TodoModel::StartDateColumn); 0509 0510 // The start date column is a QString, so fetch the to-do. 0511 // We can't compare QStrings because it won't work if the format is MM/DD/YYYY 0512 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0513 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0514 0515 if (!leftTodo || !rightTodo) { 0516 return 0; 0517 } 0518 0519 const bool leftIsEmpty = !leftTodo->hasStartDate(); 0520 const bool rightIsEmpty = !rightTodo->hasStartDate(); 0521 0522 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a start date 0523 // For sorting, no date is considered a very big date 0524 return rightIsEmpty ? -1 : 1; 0525 } else if (!leftIsEmpty) { // Both have start dates 0526 const auto leftDateTime = leftTodo->dtStart(); 0527 const auto rightDateTime = rightTodo->dtStart(); 0528 0529 if (leftDateTime == rightDateTime) { 0530 return 0; 0531 } else { 0532 return leftDateTime < rightDateTime ? -1 : 1; 0533 } 0534 } else { // Neither has a start date 0535 return 0; 0536 } 0537 } 0538 0539 int TodoSortFilterProxyModel::compareCompletedDates(const QModelIndex &left, const QModelIndex &right) const 0540 { 0541 Q_ASSERT(left.column() == Akonadi::TodoModel::CompletedDateColumn); 0542 Q_ASSERT(right.column() == Akonadi::TodoModel::CompletedDateColumn); 0543 0544 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0545 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0546 0547 if (!leftTodo || !rightTodo) { 0548 return 0; 0549 } 0550 0551 const bool leftIsEmpty = !leftTodo->hasCompletedDate(); 0552 const bool rightIsEmpty = !rightTodo->hasCompletedDate(); 0553 0554 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a completed date. 0555 // For sorting, no date is considered a very big date. 0556 return rightIsEmpty ? -1 : 1; 0557 } else if (!leftIsEmpty) { // Both have completed dates. 0558 const auto leftDateTime = leftTodo->completed(); 0559 const auto rightDateTime = rightTodo->completed(); 0560 0561 if (leftDateTime == rightDateTime) { 0562 return 0; 0563 } else { 0564 return leftDateTime < rightDateTime ? -1 : 1; 0565 } 0566 } else { // Neither has a completed date. 0567 return 0; 0568 } 0569 } 0570 0571 /* -1 - less than 0572 * 0 - equal 0573 * 1 - bigger than 0574 */ 0575 int TodoSortFilterProxyModel::compareDueDates(const QModelIndex &left, const QModelIndex &right) const 0576 { 0577 Q_ASSERT(left.column() == Akonadi::TodoModel::DueDateColumn); 0578 Q_ASSERT(right.column() == Akonadi::TodoModel::DueDateColumn); 0579 0580 // The due date column is a QString, so fetch the to-do. 0581 // We can't compare QStrings because it won't work if the format is MM/DD/YYYY 0582 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0583 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0584 Q_ASSERT(leftTodo); 0585 Q_ASSERT(rightTodo); 0586 0587 if (!leftTodo || !rightTodo) { 0588 return 0; 0589 } 0590 0591 const auto leftOverdue = leftTodo->isOverdue(); 0592 const auto rightOverdue = rightTodo->isOverdue(); 0593 0594 if (leftOverdue != rightOverdue) { 0595 return leftOverdue ? -1 : 1; 0596 } 0597 0598 const bool leftIsEmpty = !leftTodo->hasDueDate(); 0599 const bool rightIsEmpty = !rightTodo->hasDueDate(); 0600 0601 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a due date 0602 // For sorting, no date is considered a very big date 0603 return rightIsEmpty ? -1 : 1; 0604 } else if (!leftIsEmpty) { // Both have due dates 0605 const auto leftDateTime = leftTodo->dtDue(); 0606 const auto rightDateTime = rightTodo->dtDue(); 0607 0608 if (leftDateTime == rightDateTime) { 0609 return 0; 0610 } else { 0611 return leftDateTime < rightDateTime ? -1 : 1; 0612 } 0613 } else { // Neither has a due date 0614 return 0; 0615 } 0616 } 0617 0618 /* -1 - less than 0619 * 0 - equal 0620 * 1 - bigger than 0621 */ 0622 int TodoSortFilterProxyModel::compareCompletion(const QModelIndex &left, const QModelIndex &right) const 0623 { 0624 Q_ASSERT(left.column() == Akonadi::TodoModel::PercentColumn); 0625 Q_ASSERT(right.column() == Akonadi::TodoModel::PercentColumn); 0626 0627 const int leftValue = sourceModel()->data(left).toInt(); 0628 const int rightValue = sourceModel()->data(right).toInt(); 0629 0630 if (leftValue == 100 && rightValue == 100) { 0631 // Break ties with the completion date. 0632 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0633 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0634 Q_ASSERT(leftTodo); 0635 Q_ASSERT(rightTodo); 0636 if (!leftTodo || !rightTodo) { 0637 return 0; 0638 } else { 0639 return (leftTodo->completed() > rightTodo->completed()) ? -1 : 1; 0640 } 0641 } else { 0642 return (leftValue < rightValue) ? -1 : 1; 0643 } 0644 } 0645 0646 /* -1 - less than 0647 * 0 - equal 0648 * 1 - bigger than 0649 * Sort in numeric order (1 < 9) rather than priority order (lowest 9 < highest 1). 0650 * There are arguments either way, but this is consistent with KCalendarCore. 0651 */ 0652 int TodoSortFilterProxyModel::comparePriorities(const QModelIndex &left, const QModelIndex &right) const 0653 { 0654 Q_ASSERT(left.isValid()); 0655 Q_ASSERT(right.isValid()); 0656 0657 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0658 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>(); 0659 Q_ASSERT(leftTodo); 0660 Q_ASSERT(rightTodo); 0661 // Todos with no priority have a priority of 0 -- push these to list end in ascending order 0662 if (m_sortAscending && leftTodo->priority() == 0) { 0663 return 1; 0664 } else if (!leftTodo || !rightTodo || leftTodo->priority() == rightTodo->priority()) { 0665 return 0; 0666 } else if (leftTodo->priority() < rightTodo->priority()) { 0667 return -1; 0668 } else { 0669 return 1; 0670 } 0671 } 0672 0673 bool TodoSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const 0674 { 0675 // Workaround for cases where lessThan will receive invalid left index 0676 if (!left.isValid()) { 0677 return true; 0678 } 0679 0680 // To-dos without due date should appear last when sorting ascending, 0681 // so you can see the most urgent tasks first. (bug #174763) 0682 if (right.column() == Akonadi::TodoModel::DueDateColumn) { 0683 QModelIndex leftDueDateIndex = left.sibling(left.row(), Akonadi::TodoModel::DueDateColumn); // Prevent possible assert fail 0684 0685 const int comparison = compareDueDates(leftDueDateIndex, right); 0686 0687 if (comparison != 0) { 0688 return comparison == -1; 0689 } else { 0690 // Due dates are equal, but the user still expects sorting by importance 0691 // Fallback to the PriorityColumn 0692 QModelIndex leftPriorityIndex = left.sibling(left.row(), Akonadi::TodoModel::PriorityColumn); 0693 QModelIndex rightPriorityIndex = right.sibling(right.row(), Akonadi::TodoModel::PriorityColumn); 0694 const int fallbackComparison = comparePriorities(leftPriorityIndex, rightPriorityIndex); 0695 0696 if (fallbackComparison != 0) { 0697 return fallbackComparison == 1; 0698 } 0699 } 0700 } else if (right.column() == Akonadi::TodoModel::StartDateColumn) { 0701 return compareStartDates(left, right) == -1; 0702 } else if (right.column() == Akonadi::TodoModel::CompletedDateColumn) { 0703 return compareCompletedDates(left, right) == -1; 0704 } else if (right.column() == Akonadi::TodoModel::PriorityColumn) { 0705 const int comparison = comparePriorities(left, right); 0706 0707 if (comparison != 0) { 0708 return comparison == -1; 0709 } else { 0710 // Priorities are equal, but the user still expects sorting by importance 0711 // Fallback to the DueDateColumn 0712 QModelIndex leftDueDateIndex = left.sibling(left.row(), Akonadi::TodoModel::DueDateColumn); 0713 QModelIndex rightDueDateIndex = right.sibling(right.row(), Akonadi::TodoModel::DueDateColumn); 0714 const int fallbackComparison = compareDueDates(leftDueDateIndex, rightDueDateIndex); 0715 0716 if (fallbackComparison != 0) { 0717 return fallbackComparison == 1; 0718 } 0719 } 0720 } else if (right.column() == Akonadi::TodoModel::PercentColumn) { 0721 const int comparison = compareCompletion(left, right); 0722 if (comparison != 0) { 0723 return comparison == -1; 0724 } 0725 } 0726 0727 if (left.data() == right.data()) { 0728 // If both are equal, lets choose an order, otherwise Qt will display them randomly. 0729 // Fixes to-dos jumping around when you have calendar A selected, and then check/uncheck 0730 // a calendar B with no to-dos. No to-do is added/removed because calendar B is empty, 0731 // but you see the existing to-dos switching places. 0732 QModelIndex leftSummaryIndex = left.sibling(left.row(), Akonadi::TodoModel::SummaryColumn); 0733 QModelIndex rightSummaryIndex = right.sibling(right.row(), Akonadi::TodoModel::SummaryColumn); 0734 0735 // This patch is not about fallingback to the SummaryColumn for sorting. 0736 // It's about avoiding jumping due to random reasons. 0737 // That's why we ignore the sort direction... 0738 return m_sortAscending ? QSortFilterProxyModel::lessThan(leftSummaryIndex, rightSummaryIndex) 0739 : QSortFilterProxyModel::lessThan(rightSummaryIndex, leftSummaryIndex); 0740 0741 // ...so, if you have 4 to-dos, all with CompletionColumn = "55%", 0742 // and click the header multiple times, nothing will happen because 0743 // it is already sorted by Completion. 0744 } else { 0745 return QSortFilterProxyModel::lessThan(left, right); 0746 } 0747 } 0748 0749 int TodoSortFilterProxyModel::sortBy() const 0750 { 0751 return m_sortColumn; 0752 } 0753 0754 void TodoSortFilterProxyModel::setSortBy(int sortBy) 0755 { 0756 m_sortColumn = sortBy; 0757 Q_EMIT sortByChanged(); 0758 sortTodoModel(); 0759 } 0760 0761 bool TodoSortFilterProxyModel::sortAscending() const 0762 { 0763 return m_sortAscending; 0764 } 0765 0766 void TodoSortFilterProxyModel::setSortAscending(bool sortAscending) 0767 { 0768 m_sortAscending = sortAscending; 0769 Q_EMIT sortAscendingChanged(); 0770 sortTodoModel(); 0771 } 0772 0773 bool TodoSortFilterProxyModel::showCompletedSubtodosInIncomplete() const 0774 { 0775 return m_showCompletedSubtodosInIncomplete; 0776 } 0777 0778 void TodoSortFilterProxyModel::setShowCompletedSubtodosInIncomplete(bool showCompletedSubtodosInIncomplete) 0779 { 0780 m_showCompletedSubtodosInIncomplete = showCompletedSubtodosInIncomplete; 0781 Q_EMIT showCompletedSubtodosInIncompleteChanged(); 0782 0783 invalidateFilter(); 0784 } 0785 0786 Q_DECLARE_METATYPE(KCalendarCore::Incidence::Ptr) 0787