Eventviews

todoviewsortfilterproxymodel.cpp
1/*
2 This file is part of KOrganizer.
3
4 SPDX-FileCopyrightText: 2008 Thomas Thrainer <tom_t@gmx.at>
5
6 SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
7*/
8
9#include "todoviewsortfilterproxymodel.h"
10
11#include <Akonadi/TodoModel>
12#include <CalendarSupport/Utils>
13#include <KCalendarCore/CalFilter>
14
15#include <KLocalizedString>
16
17TodoViewSortFilterProxyModel::TodoViewSortFilterProxyModel(const EventViews::PrefsPtr &prefs, QObject *parent)
18 : QSortFilterProxyModel(parent)
19 , mPreferences(prefs)
20{
21}
22
23void TodoViewSortFilterProxyModel::sort(int column, Qt::SortOrder order)
24{
25 mSortOrder = order;
26 QSortFilterProxyModel::sort(column, order);
27}
28
29bool TodoViewSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
30{
31 bool ret = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
32
33 if (ret && mCalFilter) {
34 const auto incidence = sourceModel()
35 ->index(source_row, Akonadi::TodoModel::SummaryColumn, source_parent)
36 .data(Akonadi::TodoModel::TodoPtrRole)
38 if (!mCalFilter->filterIncidence(incidence)) {
39 return false;
40 }
41 }
42
43 bool returnValue = true;
44 if (ret && !mPriorities.isEmpty()) {
45 QString priorityValue = sourceModel()->index(source_row, Akonadi::TodoModel::PriorityColumn, source_parent).data(Qt::EditRole).toString();
46 returnValue = mPriorities.contains(priorityValue);
47 }
48 if (ret && !mCategories.isEmpty()) {
49 const QStringList categories = sourceModel()->index(source_row, Akonadi::TodoModel::CategoriesColumn, source_parent).data(Qt::EditRole).toStringList();
50
51 for (const QString &category : categories) {
52 if (mCategories.contains(category)) {
53 return returnValue && true;
54 }
55 }
56 ret = false;
57 }
58
59 // check if one of the children is accepted, and accept this node too if so
60 QModelIndex cur = sourceModel()->index(source_row, Akonadi::TodoModel::SummaryColumn, source_parent);
61 if (cur.isValid()) {
62 for (int r = 0; r < cur.model()->rowCount(cur); ++r) {
63 if (filterAcceptsRow(r, cur)) {
64 return true;
65 }
66 }
67 }
68
69 return ret && returnValue;
70}
71
72bool TodoViewSortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
73{
74 if (mPreferences->sortCompletedTodosSeparately() && left.column() != Akonadi::TodoModel::PercentColumn) {
75 QModelIndex cLeft = left.sibling(left.row(), Akonadi::TodoModel::PercentColumn);
76 QModelIndex cRight = right.sibling(right.row(), Akonadi::TodoModel::PercentColumn);
77
78 if (cRight.data(Qt::EditRole).toInt() == 100 && cLeft.data(Qt::EditRole).toInt() != 100) {
79 return mSortOrder == Qt::AscendingOrder ? true : false;
80 } else if (cRight.data(Qt::EditRole).toInt() != 100 && cLeft.data(Qt::EditRole).toInt() == 100) {
81 return mSortOrder == Qt::AscendingOrder ? false : true;
82 }
83 }
84
85 // To-dos without due date should appear last when sorting ascending,
86 // so you can see the most urgent tasks first. (bug #174763)
87 if (right.column() == Akonadi::TodoModel::DueDateColumn) {
88 const int comparison = compareDueDates(left, right);
89
90 if (comparison != 0) {
91 return comparison == -1;
92 } else {
93 // Due dates are equal, but the user still expects sorting by importance
94 // Fallback to the PriorityColumn
95 QModelIndex leftPriorityIndex = left.sibling(left.row(), Akonadi::TodoModel::PriorityColumn);
96 QModelIndex rightPriorityIndex = right.sibling(right.row(), Akonadi::TodoModel::PriorityColumn);
97 const int fallbackComparison = comparePriorities(leftPriorityIndex, rightPriorityIndex);
98
99 if (fallbackComparison != 0) {
100 return fallbackComparison == 1;
101 }
102 }
103 } else if (right.column() == Akonadi::TodoModel::StartDateColumn) {
104 return compareStartDates(left, right) == -1;
105 } else if (right.column() == Akonadi::TodoModel::CompletedDateColumn) {
106 return compareCompletedDates(left, right) == -1;
107 } else if (right.column() == Akonadi::TodoModel::PriorityColumn) {
108 const int comparison = comparePriorities(left, right);
109
110 if (comparison != 0) {
111 return comparison == -1;
112 } else {
113 // Priorities are equal, but the user still expects sorting by importance
114 // Fallback to the DueDateColumn
115 QModelIndex leftDueDateIndex = left.sibling(left.row(), Akonadi::TodoModel::DueDateColumn);
116 QModelIndex rightDueDateIndex = right.sibling(right.row(), Akonadi::TodoModel::DueDateColumn);
117 const int fallbackComparison = compareDueDates(leftDueDateIndex, rightDueDateIndex);
118
119 if (fallbackComparison != 0) {
120 return fallbackComparison == 1;
121 }
122 }
123 } else if (right.column() == Akonadi::TodoModel::PercentColumn) {
124 const int comparison = compareCompletion(left, right);
125 if (comparison != 0) {
126 return comparison == -1;
127 }
128 }
129
130 if (left.data() == right.data()) {
131 // If both are equal, lets choose an order, otherwise Qt will display them randomly.
132 // Fixes to-dos jumping around when you have calendar A selected, and then check/uncheck
133 // a calendar B with no to-dos. No to-do is added/removed because calendar B is empty,
134 // but you see the existing to-dos switching places.
135 QModelIndex leftSummaryIndex = left.sibling(left.row(), Akonadi::TodoModel::SummaryColumn);
136 QModelIndex rightSummaryIndex = right.sibling(right.row(), Akonadi::TodoModel::SummaryColumn);
137
138 // This patch is not about fallingback to the SummaryColumn for sorting.
139 // It's about avoiding jumping due to random reasons.
140 // That's why we ignore the sort direction...
141 return mSortOrder == Qt::AscendingOrder ? QSortFilterProxyModel::lessThan(leftSummaryIndex, rightSummaryIndex)
142 : QSortFilterProxyModel::lessThan(rightSummaryIndex, leftSummaryIndex);
143
144 // ...so, if you have 4 to-dos, all with CompletionColumn = "55%",
145 // and click the header multiple times, nothing will happen because
146 // it is already sorted by Completion.
147 } else {
148 return QSortFilterProxyModel::lessThan(left, right);
149 }
150}
151
152void TodoViewSortFilterProxyModel::setPriorityFilter(const QStringList &priorities)
153{
154 // preparing priority list for comparison
155 mPriorities.clear();
156 for (const QString &eachPriority : priorities) {
157 if (eachPriority == i18nc("priority is unspecified", "unspecified")) {
158 mPriorities.append(i18n("%1", 0));
159 } else if (eachPriority == i18nc("highest priority", "%1 (highest)", 1)) {
160 mPriorities.append(i18n("%1", 1));
161 } else if (eachPriority == i18nc("medium priority", "%1 (medium)", 5)) {
162 mPriorities.append(i18n("%1", 5));
163 } else if (eachPriority == i18nc("lowest priority", "%1 (lowest)", 9)) {
164 mPriorities.append(i18n("%1", 9));
165 } else {
166 mPriorities.append(eachPriority);
167 }
168 }
170}
171
172int TodoViewSortFilterProxyModel::compareStartDates(const QModelIndex &left, const QModelIndex &right) const
173{
174 Q_ASSERT(left.column() == Akonadi::TodoModel::StartDateColumn);
175 Q_ASSERT(right.column() == Akonadi::TodoModel::StartDateColumn);
176
177 // The start date column is a QString, so fetch the to-do.
178 // We can't compare QStrings because it won't work if the format is MM/DD/YYYY
179 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
180 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
181
182 if (!leftTodo || !rightTodo) {
183 return 0;
184 }
185
186 const bool leftIsEmpty = !leftTodo->hasStartDate();
187 const bool rightIsEmpty = !rightTodo->hasStartDate();
188
189 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a start date
190 // For sorting, no date is considered a very big date
191 return rightIsEmpty ? -1 : 1;
192 } else if (!leftIsEmpty) { // Both have start dates
193 const auto leftDateTime = leftTodo->dtStart();
194 const auto rightDateTime = rightTodo->dtStart();
195
196 if (leftDateTime == rightDateTime) {
197 return 0;
198 } else {
199 return leftDateTime < rightDateTime ? -1 : 1;
200 }
201 } else { // Neither has a start date
202 return 0;
203 }
204}
205
206int TodoViewSortFilterProxyModel::compareCompletedDates(const QModelIndex &left, const QModelIndex &right) const
207{
208 Q_ASSERT(left.column() == Akonadi::TodoModel::CompletedDateColumn);
209 Q_ASSERT(right.column() == Akonadi::TodoModel::CompletedDateColumn);
210
211 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
212 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
213
214 if (!leftTodo || !rightTodo) {
215 return 0;
216 }
217
218 const bool leftIsEmpty = !leftTodo->hasCompletedDate();
219 const bool rightIsEmpty = !rightTodo->hasCompletedDate();
220
221 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a completed date.
222 // For sorting, no date is considered a very big date.
223 return rightIsEmpty ? -1 : 1;
224 } else if (!leftIsEmpty) { // Both have completed dates.
225 const auto leftDateTime = leftTodo->completed();
226 const auto rightDateTime = rightTodo->completed();
227
228 if (leftDateTime == rightDateTime) {
229 return 0;
230 } else {
231 return leftDateTime < rightDateTime ? -1 : 1;
232 }
233 } else { // Neither has a completed date.
234 return 0;
235 }
236}
237
238void TodoViewSortFilterProxyModel::setCategoryFilter(const QStringList &categories)
239{
240 if (mCategories != categories) {
241 mCategories = categories;
243 }
244}
245
246void TodoViewSortFilterProxyModel::setCalFilter(KCalendarCore::CalFilter *filter)
247{
248 if (mCalFilter != filter) {
249 mCalFilter = filter;
251 }
252}
253
254/* -1 - less than
255 * 0 - equal
256 * 1 - bigger than
257 */
258int TodoViewSortFilterProxyModel::compareDueDates(const QModelIndex &left, const QModelIndex &right) const
259{
260 Q_ASSERT(left.column() == Akonadi::TodoModel::DueDateColumn);
261 Q_ASSERT(right.column() == Akonadi::TodoModel::DueDateColumn);
262
263 // The due date column is a QString, so fetch the to-do.
264 // We can't compare QStrings because it won't work if the format is MM/DD/YYYY
265 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
266 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
267 Q_ASSERT(leftTodo);
268 Q_ASSERT(rightTodo);
269
270 if (!leftTodo || !rightTodo) {
271 return 0;
272 }
273
274 const bool leftIsEmpty = !leftTodo->hasDueDate();
275 const bool rightIsEmpty = !rightTodo->hasDueDate();
276
277 if (leftIsEmpty != rightIsEmpty) { // One of them doesn't have a due date
278 // For sorting, no date is considered a very big date
279 return rightIsEmpty ? -1 : 1;
280 } else if (!leftIsEmpty) { // Both have due dates
281 const auto leftDateTime = leftTodo->dtDue();
282 const auto rightDateTime = rightTodo->dtDue();
283
284 if (leftDateTime == rightDateTime) {
285 return 0;
286 } else {
287 return leftDateTime < rightDateTime ? -1 : 1;
288 }
289 } else { // Neither has a due date
290 return 0;
291 }
292}
293
294/* -1 - less than
295 * 0 - equal
296 * 1 - bigger than
297 */
298int TodoViewSortFilterProxyModel::compareCompletion(const QModelIndex &left, const QModelIndex &right) const
299{
300 Q_ASSERT(left.column() == Akonadi::TodoModel::PercentColumn);
301 Q_ASSERT(right.column() == Akonadi::TodoModel::PercentColumn);
302
303 const int leftValue = sourceModel()->data(left).toInt();
304 const int rightValue = sourceModel()->data(right).toInt();
305
306 if (leftValue == 100 && rightValue == 100) {
307 // Break ties with the completion date.
308 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
309 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
310 Q_ASSERT(leftTodo);
311 Q_ASSERT(rightTodo);
312 if (!leftTodo || !rightTodo) {
313 return 0;
314 } else {
315 return (leftTodo->completed() > rightTodo->completed()) ? -1 : 1;
316 }
317 } else {
318 return (leftValue < rightValue) ? -1 : 1;
319 }
320}
321
322/* -1 - less than
323 * 0 - equal
324 * 1 - bigger than
325 * Sort in numeric order (1 < 9) rather than priority order (lowest 9 < highest 1).
326 * There are arguments either way, but this is consistent with KCalendarCore.
327 */
328int TodoViewSortFilterProxyModel::comparePriorities(const QModelIndex &left, const QModelIndex &right) const
329{
330 Q_ASSERT(left.isValid());
331 Q_ASSERT(right.isValid());
332
333 const auto leftTodo = left.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
334 const auto rightTodo = right.data(Akonadi::TodoModel::TodoPtrRole).value<KCalendarCore::Todo::Ptr>();
335 Q_ASSERT(leftTodo);
336 Q_ASSERT(rightTodo);
337 if (!leftTodo || !rightTodo || leftTodo->priority() == rightTodo->priority()) {
338 return 0;
339 } else if (leftTodo->priority() < rightTodo->priority()) {
340 return -1;
341 } else {
342 return 1;
343 }
344}
345
346#include "moc_todoviewsortfilterproxymodel.cpp"
bool filterIncidence(const Incidence::Ptr &incidence) const
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)
void append(QList< T > &&value)
void clear()
pointer data()
bool isEmpty() const const
QVariant data(int role) const const
bool isValid() const const
const QAbstractItemModel * model() const const
virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const const
virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const const
virtual int rowCount(const QModelIndex &parent) const const override
virtual void sort(int column, Qt::SortOrder order) override
QChar * data()
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
EditRole
SortOrder
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
int toInt(bool *ok) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:29 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.