MauiKit Calendar

todosortfilterproxymodel.cpp
1// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#include "todosortfilterproxymodel.h"
5#include "../filter.h"
6
7TodoSortFilterProxyModel::TodoSortFilterProxyModel(QObject *parent)
9{
10 const QString todoMimeType = QStringLiteral("application/x-vnd.akonadi.calendar.todo");
11 m_todoTreeModel.reset(new Akonadi::IncidenceTreeModel(QStringList() << todoMimeType, this));
12
13 m_baseTodoModel.reset(new Akonadi::TodoModel(this));
14 m_baseTodoModel->setSourceModel(m_todoTreeModel.data());
15 setSourceModel(m_baseTodoModel.data());
16
17 setDynamicSortFilter(true);
18 setSortCaseSensitivity(Qt::CaseInsensitive);
19 setFilterCaseSensitivity(Qt::CaseInsensitive);
20
21 KSharedConfig::Ptr config = KSharedConfig::openConfig();
22 KConfigGroup rColorsConfig(config, "Resources Colors");
23 m_colorWatcher = KConfigWatcher::create(config);
24 QObject::connect(m_colorWatcher.data(), &KConfigWatcher::configChanged, this, &TodoSortFilterProxyModel::loadColors);
25
26 loadColors();
27
28 m_dateRefreshTimer.setInterval(m_dateRefreshTimerInterval);
29 m_dateRefreshTimer.callOnTimeout(this, &TodoSortFilterProxyModel::updateDateLabels);
30 m_dateRefreshTimer.start();
31}
32
33int TodoSortFilterProxyModel::columnCount(const QModelIndex &) const
34{
35 return 1;
36}
37
38QHash<int, QByteArray> TodoSortFilterProxyModel::roleNames() const
39{
41 roleNames[Akonadi::TodoModel::SummaryRole] = "text";
42 roleNames[Roles::StartTimeRole] = "startTime";
43 roleNames[Roles::EndTimeRole] = "endTime";
44 roleNames[Roles::DisplayDueDateRole] = "displayDueDate";
45 roleNames[Roles::LocationRole] = "location";
46 roleNames[Roles::AllDayRole] = "allDay";
47 roleNames[Roles::ColorRole] = "color";
48 roleNames[Roles::CompletedRole] = "todoCompleted";
49 roleNames[Roles::PriorityRole] = "priority";
50 roleNames[Roles::CollectionIdRole] = "collectionId";
51 roleNames[Roles::DurationStringRole] = "durationString";
52 roleNames[Roles::RecursRole] = "recurs";
53 roleNames[Roles::IsOverdueRole] = "isOverdue";
54 roleNames[Roles::IncidenceIdRole] = "incidenceId";
55 roleNames[Roles::IncidenceTypeRole] = "incidenceType";
56 roleNames[Roles::IncidenceTypeStrRole] = "incidenceTypeStr";
57 roleNames[Roles::IncidenceTypeIconRole] = "incidenceTypeIcon";
58 roleNames[Roles::IncidencePtrRole] = "incidencePtr";
59 roleNames[Roles::TagsRole] = "tags";
60 roleNames[Roles::ItemRole] = "item";
61 roleNames[Roles::CategoriesRole] = "todoCategories"; // Simply 'categories' causes issues
62 roleNames[Roles::CategoriesDisplayRole] = "categoriesDisplay";
63 roleNames[Roles::TreeDepthRole] = "treeDepth";
64 roleNames[Roles::TopMostParentDueDateRole] = "topMostParentDueDate";
65 roleNames[Roles::TopMostParentSummaryRole] = "topMostParentSummary";
66 roleNames[Roles::TopMostParentPriorityRole] = "topMostParentPriority";
67
68 return roleNames;
69}
70
71QVariant TodoSortFilterProxyModel::data(const QModelIndex &index, int role) const
72{
73 if (!index.isValid() || m_calendar.isNull()) {
74 return {};
75 }
76
77 const QModelIndex sourceIndex = mapToSource(index.sibling(index.row(), 0));
78 if (!sourceIndex.isValid()) {
79 return {};
80 }
81 Q_ASSERT(sourceIndex.isValid());
82
83 auto todoItem = sourceIndex.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
84
85 if (!todoItem.isValid()) {
86 return {};
87 }
88
89 auto collectionId = todoItem.parentCollection().id();
90 auto todoPtr = Akonadi::CalendarUtils::todo(todoItem);
91
92 if (!todoPtr) {
93 return {};
94 }
95
96 if (role == Roles::StartTimeRole) {
97 return todoPtr->dtStart();
98 } else if (role == Roles::EndTimeRole) {
99 return todoPtr->dtDue();
100 } else if (role == Roles::DisplayDueDateRole) {
101 return todoDueDateDisplayString(todoPtr, DisplayDateTimeAndIfOverdue);
102 } else if (role == Roles::LocationRole) {
103 return todoPtr->location();
104 } else if (role == Roles::AllDayRole) {
105 return todoPtr->allDay();
106 } else if (role == Roles::ColorRole) {
107 QColor nullcolor;
108 return m_colors.contains(QString::number(collectionId)) ? m_colors[QString::number(collectionId)] : nullcolor;
109 } else if (role == Roles::CompletedRole) {
110 return todoPtr->isCompleted();
111 } else if (role == Roles::PriorityRole) {
112 return todoPtr->priority();
113 } else if (role == Roles::CollectionIdRole) {
114 return collectionId;
115 } else if (role == DurationStringRole) {
116 const auto duration = KCalendarCore::Duration(todoPtr->dtStart(), todoPtr->dtDue());
117
118 if (todoPtr->allDay() && !todoPtr->dtStart().isValid()) {
119 return m_format.formatSpelloutDuration(24 * 60 * 60 * 1000); // format milliseconds in 1 day
120 } else if (!todoPtr->dtStart().isValid() || duration.asSeconds() == 0) {
121 return QString();
122 }
123
124 return m_format.formatSpelloutDuration(duration.asSeconds() * 1000);
125 } else if (role == Roles::RecursRole) {
126 return todoPtr->recurs();
127 } else if (role == Roles::IsOverdueRole) {
128 return todoPtr->isOverdue();
129 } else if (role == Roles::IncidenceIdRole) {
130 return todoPtr->uid();
131 } else if (role == Roles::IncidenceTypeRole) {
132 return todoPtr->type();
133 } else if (role == Roles::IncidenceTypeStrRole) {
134 return todoPtr->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n(todoPtr->typeStr().constData());
135 } else if (role == Roles::IncidenceTypeIconRole) {
136 return todoPtr->iconName();
137 } else if (role == Roles::IncidencePtrRole) {
139 } else if (role == Roles::TagsRole) {
140 return QVariant::fromValue(todoItem.tags());
141 } else if (role == Roles::ItemRole) {
142 return QVariant::fromValue(todoItem);
143 } else if (role == Roles::CategoriesRole) {
144 return todoPtr->categories();
145 } else if (role == Roles::CategoriesDisplayRole) {
146 return todoPtr->categories().join(i18nc("List separator", ", "));
147 } else if (role == Roles::TreeDepthRole || role == TopMostParentSummaryRole || role == TopMostParentDueDateRole || role == TopMostParentPriorityRole) {
148 int depth = 0;
149 auto idx = index;
150 while (idx.parent().isValid()) {
151 idx = idx.parent();
152 depth++;
153 }
154
155 auto todo = idx.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
156
157 switch (role) {
158 case Roles::TreeDepthRole:
159 return depth;
160 case TopMostParentSummaryRole:
161 return todo->summary();
162 case TopMostParentDueDateRole: {
163 if (!todo->hasDueDate()) {
164 return i18n("No set date");
165 }
166
167 if (todo->isOverdue()) {
168 return i18n("Overdue");
169 }
170
171 const auto dateInCurrentTZ = todo->dtDue().toLocalTime().date();
172 const auto isToday = dateInCurrentTZ == QDate::currentDate();
173
174 return isToday ? i18n("Today") : todoDueDateDisplayString(todo, DisplayDateOnly);
175 }
176 case TopMostParentPriorityRole:
177 return todo->priority();
178 }
179 }
181}
182
183QString TodoSortFilterProxyModel::todoDueDateDisplayString(const KCalendarCore::Todo::Ptr todo,
184 const TodoSortFilterProxyModel::DueDateDisplayFormat format) const
185 {
186 if (!todo || !todo->hasDueDate()) {
187 return {};
188 }
189
190 const auto systemLocale = QLocale::system();
191 const auto includeTime = !todo->allDay() && format != DisplayDateOnly;
192 const auto includeOverdue = todo->isOverdue() && format == DisplayDateTimeAndIfOverdue;
193
194 const auto todoDateTimeDue = todo->dtDue().toLocalTime();
195 const auto todoDateDue = todoDateTimeDue.date();
196 const auto todoTimeDueString =
197 includeTime ? i18nc("Please retain space", " at %1", systemLocale.toString(todoDateTimeDue.time(), QLocale::NarrowFormat)) : QStringLiteral(" ");
198 const auto todoOverdueString = includeOverdue ? i18nc("Please retain parenthesis and space", " (overdue)") : QString();
199
200 const auto currentDate = QDate::currentDate();
201 const auto dateFormat = todoDateDue.year() == currentDate.year() ? QStringLiteral("dddd dd MMMM") : QStringLiteral("dddd dd MMMM yyyy");
202
203 static constexpr char translationExplainer[] =
204 "No spaces -- the (optional) %1 string, which includes the time, includes this space"
205 " as does the %2 string which is the overdue string (also optional!)";
206
207 if (currentDate == todoDateDue) {
208 return i18nc(translationExplainer, "Today%1%2", todoTimeDueString, todoOverdueString);
209 } else if (currentDate.daysTo(todoDateDue) == 1) {
210 return i18nc(translationExplainer, "Tomorrow%1%2", todoTimeDueString, todoOverdueString);
211 } else if (currentDate.daysTo(todoDateDue) == -1) {
212 return i18nc(translationExplainer, "Yesterday%1%2", todoTimeDueString, todoOverdueString);
213 }
214
215 const auto dateDueString = systemLocale.toString(todoDateDue, dateFormat);
216 return dateDueString + todoTimeDueString + todoOverdueString;
217 }
218
219 void TodoSortFilterProxyModel::updateDateLabels()
220 {
221 if (rowCount() == 0 || !sourceModel()) {
222 return;
223 }
224
225 emitDateDataChanged({});
226 sortTodoModel();
227 m_lastDateRefreshDate = QDate::currentDate();
228 }
229
230 void TodoSortFilterProxyModel::emitDateDataChanged(const QModelIndex &idx)
231 {
232 const auto idxRowCount = rowCount(idx);
233 const auto srcModel = sourceModel();
234
235 if (idxRowCount == 0) {
236 return;
237 }
238
239 const auto bottomRow = idxRowCount - 1;
240 const auto currentDate = QDate::currentDate();
241 const auto currentDateTime = QDateTime::currentDateTime();
242
243 const auto iterateOverChildren = [this, &idx, &srcModel, &currentDate, &currentDateTime](const int row) {
244 const auto childIdx = index(row, 0, idx);
245
246 const auto todo = childIdx.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
247 const auto isOverdue = todo->isOverdue();
248 const auto dtDue = todo->dtDue();
249 const auto isRecentlyOverdue = isOverdue && currentDateTime.msecsTo(dtDue) >= -m_dateRefreshTimerInterval;
250
251 if (isRecentlyOverdue || m_lastDateRefreshDate != currentDate) {
252 Q_EMIT dataChanged(childIdx, childIdx, {DisplayDueDateRole, TopMostParentDueDateRole});
253
254 // For the proxy model to re-sort items into their correct section we also need to emit a
255 // dataChanged() signal for the column we are sorting by in the source model
256 const auto srcChildIdx = mapToSource(childIdx).siblingAtColumn(Akonadi::TodoModel::DueDateColumn);
257 Q_EMIT srcModel->dataChanged(srcChildIdx, srcChildIdx, {Akonadi::TodoModel::DueDateRole});
258 }
259
260 // We recursively do the same for children
261 emitDateDataChanged(childIdx);
262 };
263
264 // This is a workaround for weird sorting behaviour. If one of the items changes because it becomes
265 // overdue, for example, the way in which we emit dataChanged() signals of the sourceModel will
266 // dictate how the model gets sorted.
267 //
268 // Example: In a case where the sort is ascending (i.e. overdue at top), a 0 to rowCount() -1 sort
269 // will move the overdue item up by only one row due to how the QSortFilterProxyModel uses lessThan().
270 // If we go the opposite way then the QSFPM calls lessThan() on the overdue item more than once, moving
271 // it upwards.
272
273 if (m_sortAscending) {
274 for (auto i = bottomRow; i >= 0; --i) {
275 iterateOverChildren(i);
276 }
277 } else {
278 for (auto i = 0; i < idxRowCount; ++i) {
279 iterateOverChildren(i);
280 }
281 }
282 }
283
284 bool TodoSortFilterProxyModel::filterAcceptsRow(int row, const QModelIndex &sourceParent) const
285 {
286 if (filterAcceptsRowCheck(row, sourceParent)) {
287 return true;
288 }
289
290 // 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
291 // 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
292 // passes the filter check. If this is not the case, we only include subtasks that pass the filter themselves.
293
294 if ((m_showCompletedSubtodosInIncomplete && m_showCompleted == ShowComplete::ShowIncompleteOnly) || m_showCompleted != ShowComplete::ShowIncompleteOnly) {
295 QModelIndex parent = sourceParent;
296 while (parent.isValid()) {
297 if (filterAcceptsRowCheck(parent.row(), parent.parent()))
298 return true;
299 parent = parent.parent();
300 }
301 }
302
303 // Accept if any child is accepted itself
304 return hasAcceptedChildren(row, sourceParent);
305 }
306
307 bool TodoSortFilterProxyModel::filterAcceptsRowCheck(int row, const QModelIndex &sourceParent) const
308 {
309 const QModelIndex sourceIndex = sourceModel()->index(row, 0, sourceParent);
310 Q_ASSERT(sourceIndex.isValid());
311
312 if (m_filterObject == nullptr) {
313 return QSortFilterProxyModel::filterAcceptsRow(row, sourceParent);
314 }
315
316 bool acceptRow = true;
317
318 if (m_filterObject->collectionId() > -1) {
319 const auto collectionId = sourceIndex.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>().parentCollection().id();
320 acceptRow = acceptRow && collectionId == m_filterObject->collectionId();
321 }
322
323 switch (m_showCompleted) {
324 case ShowComplete::ShowCompleteOnly:
325 acceptRow = acceptRow && sourceIndex.data(Akonadi::TodoModel::PercentRole).toInt() == 100;
326 break;
327 case ShowComplete::ShowIncompleteOnly:
328 acceptRow = acceptRow && sourceIndex.data(Akonadi::TodoModel::PercentRole).toInt() < 100;
329 case ShowComplete::ShowAll:
330 default:
331 break;
332 }
333
334 if (!m_filterObject->tags().isEmpty()) {
335 const auto tags = m_filterObject->tags();
336 bool containsTag = false;
337 for (const auto &tag : tags) {
338 const auto todoPtr = sourceIndex.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
339 if (todoPtr->categories().contains(tag)) {
340 containsTag = true;
341 break;
342 }
343 }
344 acceptRow = acceptRow && containsTag;
345 }
346
347 return acceptRow ? QSortFilterProxyModel::filterAcceptsRow(row, sourceParent) : acceptRow;
348 }
349
350 bool TodoSortFilterProxyModel::hasAcceptedChildren(int row, const QModelIndex &sourceParent) const
351 {
352 QModelIndex index = sourceModel()->index(row, 0, sourceParent);
353 if (!index.isValid()) {
354 return false;
355 }
356
357 int childCount = index.model()->rowCount(index);
358 if (childCount == 0)
359 return false;
360
361 for (int i = 0; i < childCount; ++i) {
362 if (filterAcceptsRowCheck(i, index))
363 return true;
364
365 if (hasAcceptedChildren(i, index))
366 return true;
367 }
368
369 return false;
370 }
371
372 Akonadi::ETMCalendar::Ptr TodoSortFilterProxyModel::calendar() const
373 {
374 return m_calendar;
375 }
376
377 void TodoSortFilterProxyModel::setCalendar(Akonadi::ETMCalendar::Ptr &calendar)
378 {
379 // No need to manually emit beginResetModel(), source model does it for us
380 m_calendar = calendar;
381 m_todoTreeModel->setSourceModel(calendar->model());
382 m_baseTodoModel->setCalendar(m_calendar);
383 Q_EMIT calendarChanged();
384 }
385
386 Akonadi::IncidenceChanger *TodoSortFilterProxyModel::incidenceChanger() const
387 {
388 return m_lastSetChanger;
389 }
390
391 void TodoSortFilterProxyModel::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
392 {
393 m_baseTodoModel->setIncidenceChanger(changer);
394 m_lastSetChanger = changer;
395
396 Q_EMIT incidenceChangerChanged();
397 }
398
399 void TodoSortFilterProxyModel::setColorCache(const QHash<QString, QColor> colorCache)
400 {
401 m_colors = colorCache;
402 }
403
404 void TodoSortFilterProxyModel::loadColors()
405 {
407 KSharedConfig::Ptr config = KSharedConfig::openConfig();
408 KConfigGroup rColorsConfig(config, "Resources Colors");
409 const QStringList colorKeyList = rColorsConfig.keyList();
410
411 for (const QString &key : colorKeyList) {
412 QColor color = rColorsConfig.readEntry(key, QColor("blue"));
413 m_colors[key] = color;
414 }
416 }
417
418 int TodoSortFilterProxyModel::showCompleted() const
419 {
420 return m_showCompleted;
421 }
422
423 void TodoSortFilterProxyModel::setShowCompleted(int showCompleted)
424 {
426 m_showCompleted = showCompleted;
427 m_showCompletedStore = showCompleted; // For when we search
429 Q_EMIT showCompletedChanged();
431
432 sortTodoModel();
433 }
434
435 Filter *TodoSortFilterProxyModel::filterObject() const
436 {
437 return m_filterObject;
438 }
439
440 void TodoSortFilterProxyModel::setFilterObject(Filter *filterObject)
441 {
442 if (m_filterObject == filterObject) {
443 return;
444 }
445
446 if (m_filterObject) {
447 disconnect(m_filterObject, nullptr, this, nullptr);
448 }
449
450 Q_EMIT filterObjectAboutToChange();
452 m_filterObject = filterObject;
453 Q_EMIT filterObjectChanged();
454
455 const auto nameFilter = m_filterObject->name();
456 const auto handleFilterNameChange = [this] {
457 Q_EMIT filterObjectAboutToChange();
458 setFilterFixedString(m_filterObject->name());
460 Q_EMIT filterObjectChanged();
461 };
462 const auto handleFilterObjectChange = [this] {
463 Q_EMIT filterObjectAboutToChange();
466 Q_EMIT filterObjectChanged();
467 };
468
469 connect(m_filterObject, &Filter::nameChanged, this, handleFilterNameChange);
470 connect(m_filterObject, &Filter::tagsChanged, this, handleFilterObjectChange);
471 connect(m_filterObject, &Filter::collectionIdChanged, this, handleFilterObjectChange);
472
473 if (!nameFilter.isEmpty()) {
474 setFilterFixedString(nameFilter);
475 }
476
478
480
481 sortTodoModel();
482 }
483
484 void TodoSortFilterProxyModel::sortTodoModel()
485 {
486 const auto order = m_sortAscending ? Qt::AscendingOrder : Qt::DescendingOrder;
487 QSortFilterProxyModel::sort(m_sortColumn, order);
488 }
489
490 void TodoSortFilterProxyModel::filterTodoName(const QString &name, const int showCompleted)
491 {
494 if (!name.isEmpty()) {
495 m_showCompleted = showCompleted;
496 } else {
497 setShowCompleted(m_showCompletedStore);
498 }
501
502 sortTodoModel();
503 }
504
505 int TodoSortFilterProxyModel::compareStartDates(const QModelIndex &left, const QModelIndex &right) const
506 {
507 Q_ASSERT(left.column() == Akonadi::TodoModel::StartDateColumn);
508 Q_ASSERT(right.column() == Akonadi::TodoModel::StartDateColumn);
509
510 // The start date column is a QString, so fetch the to-do.
511 // We can't compare QStrings because it won't work if the format is MM/DD/YYYY
512 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
513 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
514
515 if (!leftTodo || !rightTodo) {
516 return 0;
517 }
518
519 const bool leftIsEmpty = !leftTodo->hasStartDate();
520 const bool rightIsEmpty = !rightTodo->hasStartDate();
521
522 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a start date
523 // For sorting, no date is considered a very big date
524 return rightIsEmpty ? -1 : 1;
525 } else if (!leftIsEmpty) { // Both have start dates
526 const auto leftDateTime = leftTodo->dtStart();
527 const auto rightDateTime = rightTodo->dtStart();
528
529 if (leftDateTime == rightDateTime) {
530 return 0;
531 } else {
532 return leftDateTime < rightDateTime ? -1 : 1;
533 }
534 } else { // Neither has a start date
535 return 0;
536 }
537 }
538
539 int TodoSortFilterProxyModel::compareCompletedDates(const QModelIndex &left, const QModelIndex &right) const
540 {
541 Q_ASSERT(left.column() == Akonadi::TodoModel::CompletedDateColumn);
542 Q_ASSERT(right.column() == Akonadi::TodoModel::CompletedDateColumn);
543
544 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
545 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
546
547 if (!leftTodo || !rightTodo) {
548 return 0;
549 }
550
551 const bool leftIsEmpty = !leftTodo->hasCompletedDate();
552 const bool rightIsEmpty = !rightTodo->hasCompletedDate();
553
554 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a completed date.
555 // For sorting, no date is considered a very big date.
556 return rightIsEmpty ? -1 : 1;
557 } else if (!leftIsEmpty) { // Both have completed dates.
558 const auto leftDateTime = leftTodo->completed();
559 const auto rightDateTime = rightTodo->completed();
560
561 if (leftDateTime == rightDateTime) {
562 return 0;
563 } else {
564 return leftDateTime < rightDateTime ? -1 : 1;
565 }
566 } else { // Neither has a completed date.
567 return 0;
568 }
569 }
570
571 /* -1 - less than
572 * 0 - equal
573 * 1 - bigger than
574 */
575 int TodoSortFilterProxyModel::compareDueDates(const QModelIndex &left, const QModelIndex &right) const
576 {
577 Q_ASSERT(left.column() == Akonadi::TodoModel::DueDateColumn);
578 Q_ASSERT(right.column() == Akonadi::TodoModel::DueDateColumn);
579
580 // The due date column is a QString, so fetch the to-do.
581 // We can't compare QStrings because it won't work if the format is MM/DD/YYYY
582 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
583 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
584 Q_ASSERT(leftTodo);
585 Q_ASSERT(rightTodo);
586
587 if (!leftTodo || !rightTodo) {
588 return 0;
589 }
590
591 const auto leftOverdue = leftTodo->isOverdue();
592 const auto rightOverdue = rightTodo->isOverdue();
593
594 if (leftOverdue != rightOverdue) {
595 return leftOverdue ? -1 : 1;
596 }
597
598 const bool leftIsEmpty = !leftTodo->hasDueDate();
599 const bool rightIsEmpty = !rightTodo->hasDueDate();
600
601 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a due date
602 // For sorting, no date is considered a very big date
603 return rightIsEmpty ? -1 : 1;
604 } else if (!leftIsEmpty) { // Both have due dates
605 const auto leftDateTime = leftTodo->dtDue();
606 const auto rightDateTime = rightTodo->dtDue();
607
608 if (leftDateTime == rightDateTime) {
609 return 0;
610 } else {
611 return leftDateTime < rightDateTime ? -1 : 1;
612 }
613 } else { // Neither has a due date
614 return 0;
615 }
616 }
617
618 /* -1 - less than
619 * 0 - equal
620 * 1 - bigger than
621 */
622 int TodoSortFilterProxyModel::compareCompletion(const QModelIndex &left, const QModelIndex &right) const
623 {
624 Q_ASSERT(left.column() == Akonadi::TodoModel::PercentColumn);
625 Q_ASSERT(right.column() == Akonadi::TodoModel::PercentColumn);
626
627 const int leftValue = sourceModel()->data(left).toInt();
628 const int rightValue = sourceModel()->data(right).toInt();
629
630 if (leftValue == 100 && rightValue == 100) {
631 // Break ties with the completion date.
632 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
633 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
634 Q_ASSERT(leftTodo);
635 Q_ASSERT(rightTodo);
636 if (!leftTodo || !rightTodo) {
637 return 0;
638 } else {
639 return (leftTodo->completed() > rightTodo->completed()) ? -1 : 1;
640 }
641 } else {
642 return (leftValue < rightValue) ? -1 : 1;
643 }
644 }
645
646 /* -1 - less than
647 * 0 - equal
648 * 1 - bigger than
649 * Sort in numeric order (1 < 9) rather than priority order (lowest 9 < highest 1).
650 * There are arguments either way, but this is consistent with KCalendarCore.
651 */
652 int TodoSortFilterProxyModel::comparePriorities(const QModelIndex &left, const QModelIndex &right) const
653 {
654 Q_ASSERT(left.isValid());
655 Q_ASSERT(right.isValid());
656
657 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
658 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
659 Q_ASSERT(leftTodo);
660 Q_ASSERT(rightTodo);
661 // Todos with no priority have a priority of 0 -- push these to list end in ascending order
662 if (m_sortAscending && leftTodo->priority() == 0) {
663 return 1;
664 } else if (!leftTodo || !rightTodo || leftTodo->priority() == rightTodo->priority()) {
665 return 0;
666 } else if (leftTodo->priority() < rightTodo->priority()) {
667 return -1;
668 } else {
669 return 1;
670 }
671 }
672
673 bool TodoSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
674 {
675 // Workaround for cases where lessThan will receive invalid left index
676 if (!left.isValid()) {
677 return true;
678 }
679
680 // To-dos without due date should appear last when sorting ascending,
681 // so you can see the most urgent tasks first. (bug #174763)
682 if (right.column() == Akonadi::TodoModel::DueDateColumn) {
683 QModelIndex leftDueDateIndex = left.sibling(left.row(), Akonadi::TodoModel::DueDateColumn); // Prevent possible assert fail
684
685 const int comparison = compareDueDates(leftDueDateIndex, right);
686
687 if (comparison != 0) {
688 return comparison == -1;
689 } else {
690 // Due dates are equal, but the user still expects sorting by importance
691 // Fallback to the PriorityColumn
692 QModelIndex leftPriorityIndex = left.sibling(left.row(), Akonadi::TodoModel::PriorityColumn);
693 QModelIndex rightPriorityIndex = right.sibling(right.row(), Akonadi::TodoModel::PriorityColumn);
694 const int fallbackComparison = comparePriorities(leftPriorityIndex, rightPriorityIndex);
695
696 if (fallbackComparison != 0) {
697 return fallbackComparison == 1;
698 }
699 }
700 } else if (right.column() == Akonadi::TodoModel::StartDateColumn) {
701 return compareStartDates(left, right) == -1;
702 } else if (right.column() == Akonadi::TodoModel::CompletedDateColumn) {
703 return compareCompletedDates(left, right) == -1;
704 } else if (right.column() == Akonadi::TodoModel::PriorityColumn) {
705 const int comparison = comparePriorities(left, right);
706
707 if (comparison != 0) {
708 return comparison == -1;
709 } else {
710 // Priorities are equal, but the user still expects sorting by importance
711 // Fallback to the DueDateColumn
712 QModelIndex leftDueDateIndex = left.sibling(left.row(), Akonadi::TodoModel::DueDateColumn);
713 QModelIndex rightDueDateIndex = right.sibling(right.row(), Akonadi::TodoModel::DueDateColumn);
714 const int fallbackComparison = compareDueDates(leftDueDateIndex, rightDueDateIndex);
715
716 if (fallbackComparison != 0) {
717 return fallbackComparison == 1;
718 }
719 }
720 } else if (right.column() == Akonadi::TodoModel::PercentColumn) {
721 const int comparison = compareCompletion(left, right);
722 if (comparison != 0) {
723 return comparison == -1;
724 }
725 }
726
727 if (left.data() == right.data()) {
728 // If both are equal, lets choose an order, otherwise Qt will display them randomly.
729 // Fixes to-dos jumping around when you have calendar A selected, and then check/uncheck
730 // a calendar B with no to-dos. No to-do is added/removed because calendar B is empty,
731 // but you see the existing to-dos switching places.
732 QModelIndex leftSummaryIndex = left.sibling(left.row(), Akonadi::TodoModel::SummaryColumn);
733 QModelIndex rightSummaryIndex = right.sibling(right.row(), Akonadi::TodoModel::SummaryColumn);
734
735 // This patch is not about fallingback to the SummaryColumn for sorting.
736 // It's about avoiding jumping due to random reasons.
737 // That's why we ignore the sort direction...
738 return m_sortAscending ? QSortFilterProxyModel::lessThan(leftSummaryIndex, rightSummaryIndex)
739 : QSortFilterProxyModel::lessThan(rightSummaryIndex, leftSummaryIndex);
740
741 // ...so, if you have 4 to-dos, all with CompletionColumn = "55%",
742 // and click the header multiple times, nothing will happen because
743 // it is already sorted by Completion.
744 } else {
745 return QSortFilterProxyModel::lessThan(left, right);
746 }
747 }
748
749 int TodoSortFilterProxyModel::sortBy() const
750 {
751 return m_sortColumn;
752 }
753
754 void TodoSortFilterProxyModel::setSortBy(int sortBy)
755 {
756 m_sortColumn = sortBy;
757 Q_EMIT sortByChanged();
758 sortTodoModel();
759 }
760
761 bool TodoSortFilterProxyModel::sortAscending() const
762 {
763 return m_sortAscending;
764 }
765
766 void TodoSortFilterProxyModel::setSortAscending(bool sortAscending)
767 {
768 m_sortAscending = sortAscending;
769 Q_EMIT sortAscendingChanged();
770 sortTodoModel();
771 }
772
773 bool TodoSortFilterProxyModel::showCompletedSubtodosInIncomplete() const
774 {
775 return m_showCompletedSubtodosInIncomplete;
776 }
777
778 void TodoSortFilterProxyModel::setShowCompletedSubtodosInIncomplete(bool showCompletedSubtodosInIncomplete)
779 {
780 m_showCompletedSubtodosInIncomplete = showCompletedSubtodosInIncomplete;
781 Q_EMIT showCompletedSubtodosInIncompleteChanged();
782
784 }
785
786 Q_DECLARE_METATYPE(KCalendarCore::Incidence::Ptr)
787
Collection & parentCollection()
Id id() const
This class is used to enable cross-compatible filtering of data in models.
Definition filter.h:10
static Ptr create(const KSharedConfig::Ptr &config)
void configChanged(const KConfigGroup &group, const QByteArrayList &names)
QString formatSpelloutDuration(quint64 msecs) const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Todo::Ptr todo(const Akonadi::Item &item)
QJsonArray filterObject(const QJsonObject &obj)
QString name(StandardAction id)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
void layoutAboutToBeChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
virtual QHash< int, QByteArray > roleNames() const const
virtual int rowCount(const QModelIndex &parent) const const=0
QDate currentDate()
QDateTime currentDateTime()
bool contains(const Key &key) const const
bool isEmpty() const const
QLocale system()
QVariant data(int role) const const
bool isValid() const const
const QAbstractItemModel * model() const const
int row() const const
QModelIndex sibling(int row, int column) const const
QModelIndex siblingAtColumn(int column) const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
T * data() const const
virtual QVariant data(const QModelIndex &index, int role) const const override
virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const const
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const const
virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const const override
virtual int rowCount(const QModelIndex &parent) const const override
void setFilterFixedString(const QString &pattern)
virtual void sort(int column, Qt::SortOrder order) override
bool isEmpty() const const
QString number(double n, char format, int precision)
CaseInsensitive
AscendingOrder
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
QVariant fromValue(T &&value)
int toInt(bool *ok) const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:08:19 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.