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, &currentDate, &currentDateTime](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