1 /*
2 This file is part of Kontact.
3
4 SPDX-FileCopyrightText: 2003 Tobias Koenig <tokoe@kde.org>
5 SPDX-FileCopyrightText: 2005-2006, 2008-2009 Allen Winter <winter@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
8 */
9
10 #include "todosummarywidget.h"
11 #include "korganizerinterface.h"
12 #include "todoplugin.h"
13 #include <CalendarSupport/CalendarSingleton>
14 #include <CalendarSupport/Utils>
15
16 #include <Akonadi/Calendar/IncidenceChanger>
17 #include <Akonadi/Collection>
18 #include <Akonadi/ItemFetchScope>
19
20 #include <KCalUtils/IncidenceFormatter>
21
22 #include <KontactInterface/Core>
23
24 #include <KConfig>
25 #include <KConfigGroup>
26 #include <KLocalizedString>
27 #include <KUrlLabel>
28 #include <QMenu>
29
30 #include <QGridLayout>
31 #include <QLabel>
32 #include <QStyle>
33 #include <QTextDocument> // for Qt::mightBeRichText
34 #include <QVBoxLayout>
35
36 using namespace KCalUtils;
37
TodoSummaryWidget(TodoPlugin * plugin,QWidget * parent)38 TodoSummaryWidget::TodoSummaryWidget(TodoPlugin *plugin, QWidget *parent)
39 : KontactInterface::Summary(parent)
40 , mPlugin(plugin)
41 {
42 auto mainLayout = new QVBoxLayout(this);
43 mainLayout->setSpacing(3);
44 mainLayout->setContentsMargins(3, 3, 3, 3);
45
46 QWidget *header = createHeader(this, QStringLiteral("korg-todo"), i18n("Pending To-dos"));
47 mainLayout->addWidget(header);
48
49 mLayout = new QGridLayout();
50 mainLayout->addItem(mLayout);
51 mLayout->setSpacing(3);
52 mLayout->setRowStretch(6, 1);
53 mCalendar = CalendarSupport::calendarSingleton();
54
55 mChanger = new Akonadi::IncidenceChanger(parent);
56
57 connect(mCalendar.data(), &Akonadi::ETMCalendar::calendarChanged, this, &TodoSummaryWidget::updateView);
58 connect(mPlugin->core(), &KontactInterface::Core::dayChanged, this, &TodoSummaryWidget::updateView);
59
60 updateView();
61 }
62
~TodoSummaryWidget()63 TodoSummaryWidget::~TodoSummaryWidget()
64 {
65 }
66
updateView()67 void TodoSummaryWidget::updateView()
68 {
69 qDeleteAll(mLabels);
70 mLabels.clear();
71
72 KConfig config(QStringLiteral("kcmtodosummaryrc"));
73 KConfigGroup group = config.group("Days");
74 int mDaysToGo = group.readEntry("DaysToShow", 7);
75
76 group = config.group("Hide");
77 mHideInProgress = group.readEntry("InProgress", false);
78 mHideOverdue = group.readEntry("Overdue", false);
79 mHideCompleted = group.readEntry("Completed", true);
80 mHideOpenEnded = group.readEntry("OpenEnded", true);
81 mHideNotStarted = group.readEntry("NotStarted", false);
82
83 group = config.group("Groupware");
84 mShowMineOnly = group.readEntry("ShowMineOnly", false);
85
86 // for each todo,
87 // if it passes the filter, append to a list
88 // else continue
89 // sort todolist by summary
90 // sort todolist by priority
91 // sort todolist by due-date
92 // print todolist
93
94 // the filter is created by the configuration summary options, but includes
95 // days to go before to-do is due
96 // which types of to-dos to hide
97
98 KCalendarCore::Todo::List prList;
99
100 const QDate currDate = QDate::currentDate();
101 const KCalendarCore::Todo::List todos = mCalendar->todos();
102 for (const KCalendarCore::Todo::Ptr &todo : todos) {
103 if (todo->hasDueDate()) {
104 const int daysTo = currDate.daysTo(todo->dtDue().date());
105 if (daysTo >= mDaysToGo) {
106 continue;
107 }
108 }
109
110 if (mHideOverdue && todo->isOverdue()) {
111 continue;
112 }
113 if (mHideInProgress && todo->isInProgress(false)) {
114 continue;
115 }
116 if (mHideCompleted && todo->isCompleted()) {
117 continue;
118 }
119 if (mHideOpenEnded && todo->isOpenEnded()) {
120 continue;
121 }
122 if (mHideNotStarted && todo->isNotStarted(false)) {
123 continue;
124 }
125
126 prList.append(todo);
127 }
128 if (!prList.isEmpty()) {
129 prList = Akonadi::ETMCalendar::sortTodos(prList, KCalendarCore::TodoSortSummary, KCalendarCore::SortDirectionAscending);
130 prList = Akonadi::ETMCalendar::sortTodos(prList, KCalendarCore::TodoSortPriority, KCalendarCore::SortDirectionAscending);
131 prList = Akonadi::ETMCalendar::sortTodos(prList, KCalendarCore::TodoSortDueDate, KCalendarCore::SortDirectionAscending);
132 }
133
134 // The to-do print consists of the following fields:
135 // icon:due date:days-to-go:priority:summary:status
136 // where,
137 // the icon is the typical to-do icon
138 // the due date it the to-do due date
139 // the days-to-go/past is the #days until/since the to-do is due
140 // this field is left blank if the to-do is open-ended
141 // the priority is the to-do priority
142 // the summary is the to-do summary
143 // the status is comma-separated list of:
144 // overdue
145 // in-progress (started, or >0% completed)
146 // complete (100% completed)
147 // open-ended
148 // not-started (no start date and 0% completed)
149
150 int counter = 0;
151 if (!prList.isEmpty()) {
152 QPixmap pm = QIcon::fromTheme(QStringLiteral("view-calendar-tasks")).pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize));
153
154 QString str;
155
156 for (const KCalendarCore::Todo::Ptr &todo : std::as_const(prList)) {
157 bool makeBold = false;
158 int daysTo = -1;
159
160 // Optionally, show only my To-dos
161 /* if ( mShowMineOnly &&
162 !KCalendarCore::CalHelper::isMyCalendarIncidence( mCalendarAdaptor, todo.get() ) ) {
163 continue;
164 }
165 TODO: calhelper is deprecated, remove this?
166 */
167
168 // Icon label
169 auto label = new QLabel(this);
170 label->setPixmap(pm);
171 label->setMaximumWidth(label->minimumSizeHint().width());
172 mLayout->addWidget(label, counter, 0);
173 mLabels.append(label);
174
175 // Due date label
176 str.clear();
177 if (todo->hasDueDate() && todo->dtDue().date().isValid()) {
178 daysTo = currDate.daysTo(todo->dtDue().date());
179
180 if (daysTo == 0) {
181 makeBold = true;
182 str = i18nc("the to-do is due today", "Today");
183 } else if (daysTo == 1) {
184 str = i18nc("the to-do is due tomorrow", "Tomorrow");
185 } else {
186 const auto locale = QLocale::system();
187 for (int i = 3; i < 8; ++i) {
188 if (daysTo < i * 24 * 60 * 60) {
189 str = i18nc("1. weekday, 2. time",
190 "%1 %2",
191 locale.dayName(todo->dtDue().date().dayOfWeek(), QLocale::LongFormat),
192 locale.toString(todo->dtDue().time(), QLocale::ShortFormat));
193 break;
194 }
195 }
196 if (str.isEmpty()) {
197 str = locale.toString(todo->dtDue(), QLocale::ShortFormat);
198 }
199 }
200 }
201
202 label = new QLabel(str, this);
203 label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
204 mLayout->addWidget(label, counter, 1);
205 mLabels.append(label);
206 if (makeBold) {
207 QFont font = label->font();
208 font.setBold(true);
209 label->setFont(font);
210 }
211
212 // Days togo/ago label
213 str.clear();
214 if (todo->hasDueDate() && todo->dtDue().date().isValid()) {
215 if (daysTo > 0) {
216 str = i18np("in 1 day", "in %1 days", daysTo);
217 } else if (daysTo < 0) {
218 str = i18np("1 day ago", "%1 days ago", -daysTo);
219 } else {
220 str = i18nc("the to-do is due", "due");
221 }
222 }
223 label = new QLabel(str, this);
224 label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
225 mLayout->addWidget(label, counter, 2);
226 mLabels.append(label);
227
228 // Priority label
229 str = QLatin1Char('[') + QString::number(todo->priority()) + QLatin1Char(']');
230 label = new QLabel(str, this);
231 label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
232 mLayout->addWidget(label, counter, 3);
233 mLabels.append(label);
234
235 // Summary label
236 str = todo->summary();
237 if (!todo->relatedTo().isEmpty()) { // show parent only, not entire ancestry
238 KCalendarCore::Incidence::Ptr inc = mCalendar->incidence(todo->relatedTo());
239 if (inc) {
240 str = inc->summary() + QLatin1Char(':') + str;
241 }
242 }
243 if (!Qt::mightBeRichText(str)) {
244 str = str.toHtmlEscaped();
245 }
246
247 auto urlLabel = new KUrlLabel(this);
248 urlLabel->setText(str);
249 urlLabel->setUrl(todo->uid());
250 urlLabel->installEventFilter(this);
251 urlLabel->setTextFormat(Qt::RichText);
252 urlLabel->setWordWrap(true);
253 mLayout->addWidget(urlLabel, counter, 4);
254 mLabels.append(urlLabel);
255 connect(urlLabel, &KUrlLabel::leftClickedUrl, this, [this, urlLabel] {
256 viewTodo(urlLabel->url());
257 });
258 connect(urlLabel, &KUrlLabel::rightClickedUrl, this, [this, urlLabel] {
259 popupMenu(urlLabel->url());
260 });
261 // State text label
262 str = stateStr(todo);
263 label = new QLabel(str, this);
264 label->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
265 mLayout->addWidget(label, counter, 5);
266 mLabels.append(label);
267
268 counter++;
269 }
270 } // foreach
271
272 if (counter == 0) {
273 auto noTodos = new QLabel(i18np("No pending to-dos due within the next day", "No pending to-dos due within the next %1 days", mDaysToGo), this);
274 noTodos->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
275 mLayout->addWidget(noTodos, 0, 0);
276 mLabels.append(noTodos);
277 }
278
279 for (QLabel *label : std::as_const(mLabels)) {
280 label->show();
281 }
282 }
283
viewTodo(const QString & uid)284 void TodoSummaryWidget::viewTodo(const QString &uid)
285 {
286 const Akonadi::Item::Id id = mCalendar->item(uid).id();
287
288 if (id != -1) {
289 mPlugin->core()->selectPlugin(QStringLiteral("kontact_todoplugin")); // ensure loaded
290 OrgKdeKorganizerKorganizerInterface korganizer(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Korganizer"), QDBusConnection::sessionBus());
291
292 korganizer.editIncidence(QString::number(id));
293 }
294 }
295
removeTodo(const Akonadi::Item & item)296 void TodoSummaryWidget::removeTodo(const Akonadi::Item &item)
297 {
298 (void) mChanger->deleteIncidence(item);
299 }
300
completeTodo(Akonadi::Item::Id id)301 void TodoSummaryWidget::completeTodo(Akonadi::Item::Id id)
302 {
303 Akonadi::Item todoItem = mCalendar->item(id);
304
305 if (todoItem.isValid()) {
306 KCalendarCore::Todo::Ptr todo = CalendarSupport::todo(todoItem);
307 if (!todo->isReadOnly()) {
308 KCalendarCore::Todo::Ptr oldTodo(todo->clone());
309 todo->setCompleted(QDateTime::currentDateTime());
310 (void) mChanger->modifyIncidence(todoItem, oldTodo);
311 updateView();
312 }
313 }
314 }
315
popupMenu(const QString & uid)316 void TodoSummaryWidget::popupMenu(const QString &uid)
317 {
318 KCalendarCore::Todo::Ptr todo = mCalendar->todo(uid);
319 if (!todo) {
320 return;
321 }
322 Akonadi::Item item = mCalendar->item(uid);
323 QMenu popup(this);
324 QAction *editIt = popup.addAction(i18n("&Edit To-do..."));
325 QAction *delIt = popup.addAction(i18n("&Delete To-do"));
326 delIt->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
327
328 QAction *doneIt = nullptr;
329 delIt->setEnabled(mCalendar->hasRight(item, Akonadi::Collection::CanDeleteItem));
330
331 if (!todo->isCompleted()) {
332 doneIt = popup.addAction(i18n("&Mark To-do Completed"));
333 doneIt->setIcon(QIcon::fromTheme(QStringLiteral("task-complete")));
334 doneIt->setEnabled(mCalendar->hasRight(item, Akonadi::Collection::CanChangeItem));
335 }
336 // TODO: add icons to the menu actions
337
338 const QAction *selectedAction = popup.exec(QCursor::pos());
339 if (selectedAction == editIt) {
340 viewTodo(uid);
341 } else if (selectedAction == delIt) {
342 removeTodo(item);
343 } else if (doneIt && selectedAction == doneIt) {
344 completeTodo(item.id());
345 }
346 }
347
eventFilter(QObject * obj,QEvent * e)348 bool TodoSummaryWidget::eventFilter(QObject *obj, QEvent *e)
349 {
350 if (obj->inherits("KUrlLabel")) {
351 auto label = static_cast<KUrlLabel *>(obj);
352 if (e->type() == QEvent::Enter) {
353 Q_EMIT message(i18n("Edit To-do: \"%1\"", label->text()));
354 }
355 if (e->type() == QEvent::Leave) {
356 Q_EMIT message(QString());
357 }
358 }
359 return KontactInterface::Summary::eventFilter(obj, e);
360 }
361
startsToday(const KCalendarCore::Todo::Ptr & todo)362 bool TodoSummaryWidget::startsToday(const KCalendarCore::Todo::Ptr &todo)
363 {
364 return todo->hasStartDate() && todo->dtStart().date() == QDate::currentDate();
365 }
366
stateStr(const KCalendarCore::Todo::Ptr & todo)367 const QString TodoSummaryWidget::stateStr(const KCalendarCore::Todo::Ptr &todo)
368 {
369 QString str1;
370 QString str2;
371
372 if (todo->isOpenEnded()) {
373 str1 = i18n("open-ended");
374 } else if (todo->isOverdue()) {
375 str1 = QLatin1String("<font color=\"red\">") + i18nc("the to-do is overdue", "overdue") + QLatin1String("</font>");
376 } else if (startsToday(todo)) {
377 str1 = i18nc("the to-do starts today", "starts today");
378 }
379
380 if (todo->isNotStarted(false)) {
381 str2 += i18nc("the to-do has not been started yet", "not-started");
382 } else if (todo->isCompleted()) {
383 str2 += i18nc("the to-do is completed", "completed");
384 } else if (todo->isInProgress(false)) {
385 str2 += i18nc("the to-do is in-progress", "in-progress ");
386 str2 += QLatin1String(" (") + QString::number(todo->percentComplete()) + QLatin1String("%)");
387 }
388
389 if (!str1.isEmpty() && !str2.isEmpty()) {
390 str1 += i18nc("Separator for status like this: overdue, completed", ",");
391 }
392
393 return str1 + str2;
394 }
395