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