Eventviews

todoview.cpp
1/*
2 This file is part of KOrganizer.
3
4 SPDX-FileCopyrightText: 2000, 2001, 2003 Cornelius Schumacher <schumacher@kde.org>
5 SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
6 SPDX-FileCopyrightText: 2005 Rafal Rzepecki <divide@users.sourceforge.net>
7 SPDX-FileCopyrightText: 2008 Thomas Thrainer <tom_t@gmx.at>
8 SPDX-FileCopyrightText: 2013 Sérgio Martins <iamsergio@gmail.com>
9
10 SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
11*/
12
13#include "todoview.h"
14using namespace Qt::Literals::StringLiterals;
15
16#include "calendarview_debug.h"
17#include "coloredtodoproxymodel.h"
18#include "tododelegates.h"
19#include "todoviewquickaddline.h"
20#include "todoviewquicksearch.h"
21#include "todoviewsortfilterproxymodel.h"
22#include "todoviewview.h"
23
24#include <Akonadi/CalendarUtils>
25#include <Akonadi/EntityMimeTypeFilterModel>
26#include <Akonadi/EntityTreeModel>
27#include <Akonadi/TagFetchJob>
28
29#include <Akonadi/ETMViewStateSaver>
30#include <Akonadi/IncidenceTreeModel>
31#include <Akonadi/TodoModel>
32
33#include <CalendarSupport/KCalPrefs>
34
35#include <KCalendarCore/CalFormat>
36
37#include <KConfig>
38#include <KDatePickerPopup>
39#include <KDescendantsProxyModel>
40#include <KJob>
41#include <KMessageBox>
42
43#include <QGridLayout>
44#include <QHeaderView>
45#include <QIcon>
46#include <QMenu>
47#include <QSortFilterProxyModel>
48#include <QToolButton>
49
50#include <chrono>
51
52using namespace std::chrono_literals;
53
54Q_DECLARE_METATYPE(QPointer<QMenu>)
55
56using namespace EventViews;
57using namespace KCalendarCore;
58
59namespace EventViews
60{
61
62class CalendarFilterModel : public QSortFilterProxyModel
63{
65public:
66 explicit CalendarFilterModel(QObject *parent = nullptr)
68 {
69 mDescendantsProxy.setDisplayAncestorData(false);
70 QSortFilterProxyModel::setSourceModel(&mDescendantsProxy);
71 }
72
73 void setSourceModel(QAbstractItemModel *model) override
74 {
75 mDescendantsProxy.setSourceModel(model);
76 }
77
78 bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
79 {
80 const auto source_index = sourceModel()->index(source_row, 0, source_parent);
81 const auto item = sourceModel()->data(source_index, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
82
83 if (!item.isValid()) {
84 return false;
85 }
86 return mEnabledCalendars.contains(item.parentCollection().id());
87 }
88
89 void addCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
90 {
91 mEnabledCalendars.insert(calendar->collection().id());
93 }
94
95 void removeCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
96 {
97 mEnabledCalendars.remove(calendar->collection().id());
99 }
100
101private:
102 KDescendantsProxyModel mDescendantsProxy;
103 QSet<Akonadi::Collection::Id> mEnabledCalendars;
104};
105
106// We share this struct between all views, for performance and memory purposes
107class ModelStack
108{
109public:
110 ModelStack(const EventViews::PrefsPtr &preferences, QObject *parent_)
111 : todoModel(new Akonadi::TodoModel())
112 , coloredTodoModel(new ColoredTodoProxyModel(preferences))
113 , parent(parent_)
114 , prefs(preferences)
115 {
116 coloredTodoModel->setSourceModel(todoModel);
117 }
118
119 ~ModelStack()
120 {
121 delete coloredTodoModel;
122 delete todoModel;
123 delete todoTreeModel;
124 delete todoFlatModel;
125 }
126
127 void registerView(TodoView *view)
128 {
129 views << view;
130 }
131
132 void unregisterView(TodoView *view)
133 {
134 views.removeAll(view);
135 }
136
137 void setFlatView(bool flat)
138 {
139 const QString todoMimeType = QStringLiteral("application/x-vnd.akonadi.calendar.todo");
140 if (flat) {
141 for (TodoView *view : std::as_const(views)) {
142 // In flatview dropping confuses users and it's very easy to drop into a child item
144 view->setFlatView(flat, /**propagate=*/false); // So other views update their toggle icon
145
146 if (todoTreeModel) {
147 view->saveViewState(); // Save the tree state before it's gone
148 }
149 }
150
151 delete todoFlatModel;
152 todoFlatModel = new Akonadi::EntityMimeTypeFilterModel(parent);
153 todoFlatModel->addMimeTypeInclusionFilter(todoMimeType);
154 todoFlatModel->setSourceModel(model);
155 todoModel->setSourceModel(todoFlatModel);
156
157 delete todoTreeModel;
158 todoTreeModel = nullptr;
159 } else {
160 delete todoTreeModel;
161 todoTreeModel = new Akonadi::IncidenceTreeModel(QStringList() << todoMimeType, parent);
162 for (TodoView *view : std::as_const(views)) {
163 QObject::connect(todoTreeModel, &Akonadi::IncidenceTreeModel::indexChangedParent, view, &TodoView::expandIndex);
164 QObject::connect(todoTreeModel, &Akonadi::IncidenceTreeModel::batchInsertionFinished, view, &TodoView::restoreViewState);
166 view->setFlatView(flat, /**propagate=*/false); // So other views update their toggle icon
167 }
168 todoTreeModel->setSourceModel(model);
169 todoModel->setSourceModel(todoTreeModel);
170 delete todoFlatModel;
171 todoFlatModel = nullptr;
172 }
173
174 for (TodoView *view : std::as_const(views)) {
175 view->mFlatViewButton->blockSignals(true);
176 // We block signals to avoid recursion, we have two TodoViews and mFlatViewButton is synchronized
177 view->mFlatViewButton->setChecked(flat);
178 view->mFlatViewButton->blockSignals(false);
179 view->mView->setRootIsDecorated(!flat);
180 view->restoreViewState();
181 }
182
183 prefs->setFlatListTodo(flat);
184 prefs->writeConfig();
185 }
186
187 void setModel(QAbstractItemModel *model)
188 {
189 this->model = model;
190 if (todoTreeModel) {
191 todoTreeModel->setSourceModel(this->model);
192 }
193 }
194
195 bool isFlatView() const
196 {
197 return todoFlatModel != nullptr;
198 }
199
200 Akonadi::TodoModel *const todoModel;
201 ColoredTodoProxyModel *const coloredTodoModel;
202 QList<TodoView *> views;
203 QObject *parent = nullptr;
204
205 QAbstractItemModel *model = nullptr;
206 Akonadi::IncidenceTreeModel *todoTreeModel = nullptr;
207 Akonadi::EntityMimeTypeFilterModel *todoFlatModel = nullptr;
208 EventViews::PrefsPtr prefs;
209};
210}
211
212// Don't use K_GLOBAL_STATIC, see QTBUG-22667
213static ModelStack *sModels = nullptr;
214
215TodoView::TodoView(const EventViews::PrefsPtr &prefs, bool sidebarView, QWidget *parent)
216 : EventView(parent)
217 , mCalendarFilterModel(std::make_unique<CalendarFilterModel>())
218 , mQuickSearch(nullptr)
219 , mQuickAdd(nullptr)
220 , mTreeStateRestorer(nullptr)
221 , mSidebarView(sidebarView)
222 , mResizeColumnsScheduled(false)
223{
224 mResizeColumnsTimer = new QTimer(this);
225 connect(mResizeColumnsTimer, &QTimer::timeout, this, &TodoView::resizeColumns);
226 mResizeColumnsTimer->setInterval(100ms); // so we don't overdue it when user resizes window manually
227 mResizeColumnsTimer->setSingleShot(true);
228
229 setPreferences(prefs);
230 if (!sModels) {
231 sModels = new ModelStack(prefs, parent);
232 connect(sModels->todoModel, &Akonadi::TodoModel::dropOnSelfRejected, this, []() {
233 KMessageBox::information(nullptr,
234 i18nc("@info", "Cannot move to-do to itself or a child of itself."),
235 i18nc("@title:window", "Drop To-do"),
236 QStringLiteral("NoDropTodoOntoItself"));
237 });
238 }
239 sModels->registerView(this);
240 sModels->setModel(mCalendarFilterModel.get());
241
242 mProxyModel = new TodoViewSortFilterProxyModel(preferences(), this);
243 mProxyModel->setSourceModel(sModels->coloredTodoModel);
244 mProxyModel->setFilterKeyColumn(Akonadi::TodoModel::SummaryColumn);
245 mProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
246 mProxyModel->setSortRole(Qt::EditRole);
247 connect(mProxyModel, &TodoViewSortFilterProxyModel::rowsInserted, this, &TodoView::onRowsInserted);
248
249 if (!mSidebarView) {
250 mQuickSearch = new TodoViewQuickSearch(this);
251 mQuickSearch->setVisible(prefs->enableTodoQuickSearch());
252 connect(mQuickSearch,
253 &TodoViewQuickSearch::searchTextChanged,
254 mProxyModel,
255 qOverload<const QString &>(&QSortFilterProxyModel::setFilterRegularExpression));
256 connect(mQuickSearch, &TodoViewQuickSearch::searchTextChanged, this, &TodoView::restoreViewState);
257 connect(mQuickSearch, &TodoViewQuickSearch::filterCategoryChanged, mProxyModel, &TodoViewSortFilterProxyModel::setCategoryFilter);
258 connect(mQuickSearch, &TodoViewQuickSearch::filterCategoryChanged, this, &TodoView::restoreViewState);
259 connect(mQuickSearch, &TodoViewQuickSearch::filterPriorityChanged, mProxyModel, &TodoViewSortFilterProxyModel::setPriorityFilter);
260 connect(mQuickSearch, &TodoViewQuickSearch::filterPriorityChanged, this, &TodoView::restoreViewState);
261 }
262
263 mView = new TodoViewView(this);
264 mView->setModel(mProxyModel);
265
266 mView->setContextMenuPolicy(Qt::CustomContextMenu);
267
268 mView->setSortingEnabled(true);
269
270 mView->setAutoExpandDelay(250);
271 mView->setDragDropMode(QAbstractItemView::DragDrop);
272
273 mView->setExpandsOnDoubleClick(false);
275
276 connect(mView->header(), &QHeaderView::geometriesChanged, this, &TodoView::scheduleResizeColumns);
277 connect(mView, &TodoViewView::visibleColumnCountChanged, this, &TodoView::resizeColumns);
278
279 auto richTextDelegate = new TodoRichTextDelegate(mView);
280 mView->setItemDelegateForColumn(Akonadi::TodoModel::SummaryColumn, richTextDelegate);
281 mView->setItemDelegateForColumn(Akonadi::TodoModel::DescriptionColumn, richTextDelegate);
282
283 auto priorityDelegate = new TodoPriorityDelegate(mView);
284 mView->setItemDelegateForColumn(Akonadi::TodoModel::PriorityColumn, priorityDelegate);
285
286 auto startDateDelegate = new TodoDueDateDelegate(mView);
287 mView->setItemDelegateForColumn(Akonadi::TodoModel::StartDateColumn, startDateDelegate);
288
289 auto dueDateDelegate = new TodoDueDateDelegate(mView);
290 mView->setItemDelegateForColumn(Akonadi::TodoModel::DueDateColumn, dueDateDelegate);
291
292 auto completeDelegate = new TodoCompleteDelegate(mView);
293 mView->setItemDelegateForColumn(Akonadi::TodoModel::PercentColumn, completeDelegate);
294
295 mCategoriesDelegate = new TodoCategoriesDelegate(mView);
296 mView->setItemDelegateForColumn(Akonadi::TodoModel::CategoriesColumn, mCategoriesDelegate);
297
298 connect(mView, &TodoViewView::customContextMenuRequested, this, &TodoView::contextMenu);
299 connect(mView, &TodoViewView::doubleClicked, this, &TodoView::itemDoubleClicked);
300
301 connect(mView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TodoView::currentChanged);
302
303 mQuickAdd = new TodoViewQuickAddLine(this);
304 mQuickAdd->setClearButtonEnabled(true);
305 mQuickAdd->setVisible(preferences()->enableQuickTodo());
306 mQuickAdd->setToolTip(i18nc("@info:tooltip", "Create an open-ended to-do"));
307 mQuickAdd->setWhatsThis(xi18nc("@info:whatsthis",
308 "Enter the summary for a new to-do. <p><note>The new to-do will be open-ended meaning that it has no start or end times nor "
309 "will it have a reminder or recurrence. Edit the newly created if you want to add more properties.</note></p>"));
310 connect(mQuickAdd, &TodoViewQuickAddLine::returnPressed, this, &TodoView::addQuickTodo);
311
312 mFullViewButton = nullptr;
313 if (!mSidebarView) {
314 mFullViewButton = new QToolButton(this);
315 mFullViewButton->setAutoRaise(true);
316 mFullViewButton->setCheckable(true);
317
318 mFullViewButton->setToolTip(i18nc("@info:tooltip", "Display to-do list in a full window"));
319 mFullViewButton->setWhatsThis(i18nc("@info:whatsthis", "Checking this option will cause the to-do view to use the full window."));
320 }
321 mFlatViewButton = new QToolButton(this);
322 mFlatViewButton->setAutoRaise(true);
323 mFlatViewButton->setCheckable(true);
324 mFlatViewButton->setToolTip(i18nc("@info:tooltip", "Display to-dos in a flat list or a tree"));
325 mFlatViewButton->setWhatsThis(i18nc("@info:whatsthis",
326 "Checking this button will cause the to-dos to be displayed either as a "
327 "flat list or a hierarchical tree where the parental "
328 "relationships are removed."));
329
330 connect(mFlatViewButton, &QToolButton::toggled, this, [this](bool flatView) {
331 setFlatView(flatView, true);
332 });
333 if (mFullViewButton) {
334 connect(mFullViewButton, &QToolButton::toggled, this, &TodoView::setFullView);
335 }
336
337 auto layout = new QGridLayout(this);
338 layout->setContentsMargins({});
339 if (!mSidebarView) {
340 layout->addWidget(mQuickSearch, 0, 0, 1, 2);
341 }
342 layout->addWidget(mView, 1, 0, 1, 2);
343 layout->setRowStretch(1, 1);
344 layout->addWidget(mQuickAdd, 2, 0);
345
346 // Dummy layout just to add a few px of right margin so the checkbox is aligned
347 // with the QAbstractItemView's viewport.
348 auto dummyLayout = new QHBoxLayout();
349 dummyLayout->setContentsMargins(0, 0, mView->frameWidth() /*right*/, 0);
350 if (!mSidebarView) {
351 auto f = new QFrame(this);
352 f->setFrameShape(QFrame::VLine);
353 f->setFrameShadow(QFrame::Sunken);
354 dummyLayout->addWidget(f);
355 dummyLayout->addWidget(mFullViewButton);
356 }
357 dummyLayout->addWidget(mFlatViewButton);
358
359 layout->addLayout(dummyLayout, 2, 1);
360
361 // ---------------- POPUP-MENUS -----------------------
362 mItemPopupMenu = new QMenu(this);
363
364 mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("document-preview")),
365 i18nc("@action:inmenu show the to-do", "&Show"),
366 this,
367 &TodoView::showTodo);
368
369 QAction *a = mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("document-edit")),
370 i18nc("@action:inmenu edit the to-do", "&Edit…"),
371 this,
372 &TodoView::editTodo);
373 mItemPopupMenuReadWriteEntries << a;
374 mItemPopupMenuItemOnlyEntries << a;
375
376 a = mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")),
377 i18nc("@action:inmenu delete the to-do", "&Delete"),
378 this,
379 &TodoView::deleteTodo);
380 mItemPopupMenuReadWriteEntries << a;
381 mItemPopupMenuItemOnlyEntries << a;
382
383 mItemPopupMenu->addSeparator();
384
385 mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("document-print")),
386 i18nc("@action:inmenu print the to-do", "&Print…"),
387 this,
388 &TodoView::printTodo);
389
390 mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("document-print-preview")),
391 i18nc("@action:inmenu print preview the to-do", "Print Previe&w…"),
392 this,
393 &TodoView::printPreviewTodo);
394
395 mItemPopupMenu->addSeparator();
396
397 mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("view-calendar-tasks")),
398 i18nc("@action:inmenu create a new to-do", "New &To-do…"),
399 this,
400 &TodoView::newTodo);
401
402 a = mItemPopupMenu->addAction(i18nc("@action:inmenu create a new sub-to-do", "New Su&b-to-do…"), this, &TodoView::newSubTodo);
403 mItemPopupMenuReadWriteEntries << a;
404 mItemPopupMenuItemOnlyEntries << a;
405
406 mMakeTodoIndependent = mItemPopupMenu->addAction(i18nc("@action:inmenu", "&Make this To-do Independent"), this, &TodoView::unSubTodoSignal);
407
408 mMakeSubtodosIndependent = mItemPopupMenu->addAction(i18nc("@action:inmenu", "Make all Sub-to-dos &Independent"), this, &TodoView::unAllSubTodoSignal);
409
410 mItemPopupMenuItemOnlyEntries << mMakeTodoIndependent;
411 mItemPopupMenuItemOnlyEntries << mMakeSubtodosIndependent;
412
413 mItemPopupMenuReadWriteEntries << mMakeTodoIndependent;
414 mItemPopupMenuReadWriteEntries << mMakeSubtodosIndependent;
415
416 mItemPopupMenu->addSeparator();
417
418 a = mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("appointment-new")),
419 i18nc("@action:inmenu", "Create Event from To-do"),
420 this,
421 qOverload<>(&TodoView::createEvent));
422 a->setObjectName("createevent"_L1);
423 mItemPopupMenuReadWriteEntries << a;
424 mItemPopupMenuItemOnlyEntries << a;
425
426 mItemPopupMenu->addSeparator();
427
429 mCopyPopupMenu->setTitle(i18nc("@title:menu", "&Copy To"));
430
431 connect(mCopyPopupMenu, &KDatePickerPopup::dateChanged, this, &TodoView::copyTodoToDate);
432
433 connect(mCopyPopupMenu, &KDatePickerPopup::dateChanged, mItemPopupMenu, &QMenu::hide);
434
436 mMovePopupMenu->setTitle(i18nc("@title:menu", "&Move To"));
437
438 connect(mMovePopupMenu, &KDatePickerPopup::dateChanged, this, &TodoView::setNewDate);
439 connect(mView->startPopupMenu(), &KDatePickerPopup::dateChanged, this, &TodoView::setStartDate);
440
441 connect(mMovePopupMenu, &KDatePickerPopup::dateChanged, mItemPopupMenu, &QMenu::hide);
442
443 mItemPopupMenu->insertMenu(nullptr, mCopyPopupMenu);
444 mItemPopupMenu->insertMenu(nullptr, mMovePopupMenu);
445
446 mItemPopupMenu->addSeparator();
447 mItemPopupMenu->addAction(i18nc("@action:inmenu delete completed to-dos", "Pur&ge Completed"), this, &TodoView::purgeCompletedSignal);
448
449 mPriorityPopupMenu = new QMenu(this);
450 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu unspecified priority", "unspecified"))] = 0;
451 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu highest priority", "1 (highest)"))] = 1;
452 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=2", "2"))] = 2;
453 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=3", "3"))] = 3;
454 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=4", "4"))] = 4;
455 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu medium priority", "5 (medium)"))] = 5;
456 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=6", "6"))] = 6;
457 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=7", "7"))] = 7;
458 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=8", "8"))] = 8;
459 mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu lowest priority", "9 (lowest)"))] = 9;
460 connect(mPriorityPopupMenu, &QMenu::triggered, this, &TodoView::setNewPriority);
461
462 mPercentageCompletedPopupMenu = new QMenu(this);
463 for (int i = 0; i <= 100; i += 10) {
464 const QString label = QStringLiteral("%1 %").arg(i);
465 mPercentage[mPercentageCompletedPopupMenu->addAction(label)] = i;
466 }
467 connect(mPercentageCompletedPopupMenu, &QMenu::triggered, this, &TodoView::setNewPercentage);
468
469 setMinimumHeight(50);
470
471 // Initialize our proxy models
472 setFlatView(preferences()->flatListTodo());
473 setFullView(preferences()->fullViewTodo());
474
475 updateConfig();
476}
477
478TodoView::~TodoView()
479{
480 saveViewState();
481
482 sModels->unregisterView(this);
483 if (sModels->views.isEmpty()) {
484 delete sModels;
485 sModels = nullptr;
486 }
487}
488
489void TodoView::expandIndex(const QModelIndex &index)
490{
491 QModelIndex todoModelIndex = sModels->todoModel->mapFromSource(index);
492 Q_ASSERT(todoModelIndex.isValid());
493 const auto coloredIndex = sModels->coloredTodoModel->mapFromSource(todoModelIndex);
494 Q_ASSERT(coloredIndex.isValid());
495 QModelIndex realIndex = mProxyModel->mapFromSource(coloredIndex);
496 Q_ASSERT(realIndex.isValid());
497 while (realIndex.isValid()) {
498 mView->expand(realIndex);
499 realIndex = mProxyModel->parent(realIndex);
500 }
501}
502
503void TodoView::setModel(QAbstractItemModel *model)
504{
505 EventView::setModel(model);
506
507 mCalendarFilterModel->setSourceModel(model);
508 restoreViewState();
509}
510
511void TodoView::addCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
512{
513 EventView::addCalendar(calendar);
514 mCalendarFilterModel->addCalendar(calendar);
515 if (calendars().size() == 1) {
516 mProxyModel->setCalFilter(calendar->filter());
517 }
518}
519
520void TodoView::removeCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
521{
522 mCalendarFilterModel->removeCalendar(calendar);
523 EventView::removeCalendar(calendar);
524}
525
527{
529 const QModelIndexList selection = mView->selectionModel()->selectedRows();
530 ret.reserve(selection.count());
531 for (const QModelIndex &mi : selection) {
532 ret << mi.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
533 }
534 return ret;
535}
536
538{
539 // The todo view only lists todo's. It's probably not a good idea to
540 // return something about the selected todo here, because it has got
541 // a couple of dates (creation, due date, completion date), and the
542 // caller could not figure out what he gets. So just return an empty list.
543 return {};
544}
545
546void TodoView::saveLayout(KConfig *config, const QString &group) const
547{
548 KConfigGroup cfgGroup = config->group(group);
549 QHeaderView *header = mView->header();
550
551 QVariantList columnVisibility;
552 QVariantList columnOrder;
553 QVariantList columnWidths;
554 const int headerCount = header->count();
555 columnVisibility.reserve(headerCount);
556 columnWidths.reserve(headerCount);
557 columnOrder.reserve(headerCount);
558 for (int i = 0; i < headerCount; ++i) {
559 columnVisibility << QVariant(!mView->isColumnHidden(i));
560 columnWidths << QVariant(header->sectionSize(i));
561 columnOrder << QVariant(header->visualIndex(i));
562 }
563 cfgGroup.writeEntry("ColumnVisibility", columnVisibility);
564 cfgGroup.writeEntry("ColumnOrder", columnOrder);
565 cfgGroup.writeEntry("ColumnWidths", columnWidths);
566
567 cfgGroup.writeEntry("SortAscending", (int)header->sortIndicatorOrder());
568 if (header->isSortIndicatorShown()) {
569 cfgGroup.writeEntry("SortColumn", header->sortIndicatorSection());
570 } else {
571 cfgGroup.writeEntry("SortColumn", -1);
572 }
573
574 if (!mSidebarView) {
575 preferences()->setFullViewTodo(mFullViewButton->isChecked());
576 }
577 preferences()->setFlatListTodo(mFlatViewButton->isChecked());
578}
579
580void TodoView::restoreLayout(KConfig *config, const QString &group, bool minimalDefaults)
581{
582 KConfigGroup cfgGroup = config->group(group);
583 QHeaderView *header = mView->header();
584
585 QVariantList columnVisibility = cfgGroup.readEntry("ColumnVisibility", QVariantList());
586 QVariantList columnOrder = cfgGroup.readEntry("ColumnOrder", QVariantList());
587 QVariantList columnWidths = cfgGroup.readEntry("ColumnWidths", QVariantList());
588
589 if (columnVisibility.isEmpty()) {
590 // if config is empty then use default settings
591 mView->hideColumn(Akonadi::TodoModel::RecurColumn);
592 mView->hideColumn(Akonadi::TodoModel::DescriptionColumn);
593 mView->hideColumn(Akonadi::TodoModel::CalendarColumn);
594 mView->hideColumn(Akonadi::TodoModel::CompletedDateColumn);
595
596 if (minimalDefaults) {
597 mView->hideColumn(Akonadi::TodoModel::PriorityColumn);
598 mView->hideColumn(Akonadi::TodoModel::PercentColumn);
599 mView->hideColumn(Akonadi::TodoModel::DescriptionColumn);
600 mView->hideColumn(Akonadi::TodoModel::CategoriesColumn);
601 }
602
603 // We don't have any incidences (content) yet, so we delay resizing
604 QTimer::singleShot(0, this, &TodoView::resizeColumns);
605 } else {
606 for (int i = 0; i < header->count() && i < columnOrder.size() && i < columnWidths.size() && i < columnVisibility.size(); i++) {
607 bool visible = columnVisibility[i].toBool();
608 int width = columnWidths[i].toInt();
609 int order = columnOrder[i].toInt();
610
611 header->resizeSection(i, width);
612 header->moveSection(header->visualIndex(i), order);
613 if (i != 0 && !visible) {
614 mView->hideColumn(i);
615 }
616 }
617 }
618
619 int sortOrder = cfgGroup.readEntry("SortAscending", (int)Qt::AscendingOrder);
620 int sortColumn = cfgGroup.readEntry("SortColumn", -1);
621 if (sortColumn >= 0) {
622 mView->sortByColumn(sortColumn, (Qt::SortOrder)sortOrder);
623 }
624
625 mFlatViewButton->setChecked(cfgGroup.readEntry("FlatView", false));
626}
627
628void TodoView::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
629{
631 sModels->todoModel->setIncidenceChanger(changer);
632}
633
634void TodoView::showDates(const QDate &start, const QDate &end, const QDate &)
635{
636 // There is nothing to do here for the Todo View
637 Q_UNUSED(start)
638 Q_UNUSED(end)
639}
640
641void TodoView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date)
642{
643 Q_UNUSED(incidenceList)
644 Q_UNUSED(date)
645}
646
647void TodoView::updateView()
648{
649 if (calendars().empty()) {
650 return;
651 }
652
653 auto calendar = calendars().first();
654 mProxyModel->setCalFilter(calendar->filter());
655}
656
657void TodoView::changeIncidenceDisplay(const Akonadi::Item &, Akonadi::IncidenceChanger::ChangeType)
658{
659 // Don't do anything, model is connected to ETM, it's up to date
660}
661
662void TodoView::updateConfig()
663{
664 Q_ASSERT(preferences());
665 if (!mSidebarView && mQuickSearch) {
666 mQuickSearch->setVisible(preferences()->enableTodoQuickSearch());
667 }
668
669 if (mQuickAdd) {
670 mQuickAdd->setVisible(preferences()->enableQuickTodo());
671 }
672
673 if (mProxyModel) {
674 mProxyModel->invalidate();
675 }
676
677 updateView();
678}
679
680void TodoView::clearSelection()
681{
682 mView->selectionModel()->clearSelection();
683}
684
685void TodoView::addTodo(const QString &summary, const Akonadi::Item &parentItem, const QStringList &categories)
686{
687 const QString summaryTrimmed = summary.trimmed();
688 if (!changer() || summaryTrimmed.isEmpty()) {
689 return;
690 }
691
693
694 KCalendarCore::Todo::Ptr todo(new KCalendarCore::Todo);
695 todo->setSummary(summaryTrimmed);
696 todo->setOrganizer(Person(CalendarSupport::KCalPrefs::instance()->fullName(), CalendarSupport::KCalPrefs::instance()->email()));
697
698 todo->setCategories(categories);
699
700 if (parent && !parent->hasRecurrenceId()) {
701 todo->setRelatedTo(parent->uid());
702 }
703
704 /* A todo without a start datetime can't have a reminder so don't bother adding one
705 if (CalendarSupport::KCalPrefs::instance()->defaultTodoReminders()) {
706 KCalendarCore::Alarm::Ptr alarm = todo->newAlarm();
707 CalendarSupport::createAlarmReminder(alarm, todo->type());
708 }
709 */
710
711 // TODO: use the default todo calendar id (once we have one)
712 // Akonadi::Collection collection = Akonadi::EntityTreeModel::updatedCollection(model(), CalendarSupport::KCalPrefs::instance()->defaultCalendarId());
713 Akonadi::Collection collection;
714 // Use the same collection of the parent.
715 if (parentItem.isValid()) {
716 // Don't use parentCollection() since it might be a virtual collection
717 collection = Akonadi::EntityTreeModel::updatedCollection(model(), parentItem.storageCollectionId());
718 }
719
720 changer()->createIncidence(todo, collection, this);
721}
722
723void TodoView::addQuickTodo(Qt::KeyboardModifiers modifiers)
724{
725 if (modifiers == Qt::NoModifier) {
726 /*const QModelIndex index = */
727 addTodo(mQuickAdd->text(), Akonadi::Item(), mProxyModel->categories());
728 } else if (modifiers == Qt::ControlModifier) {
729 QModelIndexList selection = mView->selectionModel()->selectedRows();
730 if (selection.count() != 1) {
731 qCWarning(CALENDARVIEW_LOG) << "No to-do selected" << selection;
732 return;
733 }
734 const QModelIndex idx = mProxyModel->mapToSource(selection[0]);
735 mView->expand(selection[0]);
736 const auto parent = sModels->coloredTodoModel->data(idx, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
737 addTodo(mQuickAdd->text(), parent, mProxyModel->categories());
738 } else {
739 return;
740 }
741 mQuickAdd->setText(QString());
742}
743
744void TodoView::contextMenu(QPoint pos)
745{
746 const bool hasItem = mView->indexAt(pos).isValid();
747 Incidence::Ptr incidencePtr;
748
749 for (QAction *entry : std::as_const(mItemPopupMenuItemOnlyEntries)) {
750 bool enable;
751
752 if (hasItem) {
753 const Akonadi::Item::List incidences = selectedIncidences();
754
755 if (incidences.isEmpty()) {
756 enable = false;
757 } else {
758 Akonadi::Item item = incidences.first();
759 incidencePtr = Akonadi::CalendarUtils::incidence(item);
760
761 // Action isn't RO, it can change the incidence, "Edit" for example.
762 const bool actionIsRw = mItemPopupMenuReadWriteEntries.contains(entry);
763
764 const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), item.storageCollectionId());
765 const bool incidenceIsRO = (collection.rights() & Akonadi::Collection::CanChangeItem) == 0;
766
767 enable = hasItem && (!actionIsRw || !incidenceIsRO);
768 }
769 } else {
770 enable = false;
771 }
772
773 entry->setEnabled(enable);
774 }
775 mCopyPopupMenu->setEnabled(hasItem);
776 mMovePopupMenu->setEnabled(hasItem);
777
778 if (hasItem) {
779 if (incidencePtr) {
780 const bool hasRecId = incidencePtr->hasRecurrenceId();
781 const bool hasSubtodos = mView->model()->hasChildren(mView->indexAt(pos));
782
783 mMakeSubtodosIndependent->setEnabled(!hasRecId && hasSubtodos);
784 mMakeTodoIndependent->setEnabled(!hasRecId && !incidencePtr->relatedTo().isEmpty());
785 }
786
787 switch (mView->indexAt(pos).column()) {
788 case Akonadi::TodoModel::PriorityColumn:
789 mPriorityPopupMenu->popup(mView->viewport()->mapToGlobal(pos));
790 break;
791 case Akonadi::TodoModel::PercentColumn:
792 mPercentageCompletedPopupMenu->popup(mView->viewport()->mapToGlobal(pos));
793 break;
794 case Akonadi::TodoModel::StartDateColumn:
795 mView->startPopupMenu()->popup(mView->viewport()->mapToGlobal(pos));
796 break;
797 case Akonadi::TodoModel::DueDateColumn:
798 mMovePopupMenu->popup(mView->viewport()->mapToGlobal(pos));
799 break;
800 case Akonadi::TodoModel::CategoriesColumn:
801 createCategoryPopupMenu()->popup(mView->viewport()->mapToGlobal(pos));
802 break;
803 default:
804 mItemPopupMenu->popup(mView->viewport()->mapToGlobal(pos));
805 break;
806 }
807 } else {
808 mItemPopupMenu->popup(mView->viewport()->mapToGlobal(pos));
809 }
810}
811
812void TodoView::currentChanged(const QModelIndex &current, const QModelIndex &previous)
813{
814 Q_UNUSED(previous);
815 if (!current.isValid()) {
816 Q_EMIT incidenceSelected(Akonadi::Item(), QDate());
817 return;
818 }
819
820 const auto todoItem = current.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
821
822 if (selectedIncidenceDates().isEmpty()) {
823 Q_EMIT incidenceSelected(todoItem, QDate());
824 } else {
825 Q_EMIT incidenceSelected(todoItem, selectedIncidenceDates().at(0));
826 }
827}
828
829void TodoView::showTodo()
830{
831 QModelIndexList selection = mView->selectionModel()->selectedRows();
832 if (selection.size() != 1) {
833 return;
834 }
835
836 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
837
838 Q_EMIT showIncidenceSignal(todoItem);
839}
840
841void TodoView::editTodo()
842{
843 QModelIndexList selection = mView->selectionModel()->selectedRows();
844 if (selection.size() != 1) {
845 return;
846 }
847
848 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
849 Q_EMIT editIncidenceSignal(todoItem);
850}
851
852void TodoView::deleteTodo()
853{
854 QModelIndexList selection = mView->selectionModel()->selectedRows();
855 if (selection.size() == 1) {
856 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
857
858 if (!changer()->deletedRecently(todoItem.id())) {
860 }
861 }
862}
863
864void TodoView::newTodo()
865{
866 Q_EMIT newTodoSignal(QDate::currentDate().addDays(7));
867}
868
869void TodoView::newSubTodo()
870{
871 QModelIndexList selection = mView->selectionModel()->selectedRows();
872 if (selection.size() == 1) {
873 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
874
875 Q_EMIT newSubTodoSignal(todoItem);
876 } else {
877 // This never happens
878 qCWarning(CALENDARVIEW_LOG) << "Selection size isn't 1";
879 }
880}
881
882void TodoView::copyTodoToDate(QDate date)
883{
884 if (!changer()) {
885 return;
886 }
887
888 QModelIndexList selection = mView->selectionModel()->selectedRows();
889 if (selection.size() != 1) {
890 return;
891 }
892
893 const QModelIndex origIndex = mProxyModel->mapToSource(selection[0]);
894 Q_ASSERT(origIndex.isValid());
895
896 const auto origItem = sModels->coloredTodoModel->data(origIndex, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
897
899 if (!orig) {
900 return;
901 }
902
903 KCalendarCore::Todo::Ptr todo(orig->clone());
904
906
907 QDateTime due = todo->dtDue();
908 due.setDate(date);
909 todo->setDtDue(due);
910
911 changer()->createIncidence(todo, Akonadi::Collection(), this);
912}
913
914void TodoView::scheduleResizeColumns()
915{
916 mResizeColumnsScheduled = true;
917 mResizeColumnsTimer->start(); // restarts the timer if already active
918}
919
920void TodoView::itemDoubleClicked(const QModelIndex &index)
921{
922 if (index.isValid()) {
923 QModelIndex summary = index.sibling(index.row(), Akonadi::TodoModel::SummaryColumn);
924 if (summary.flags() & Qt::ItemIsEditable) {
925 editTodo();
926 } else {
927 showTodo();
928 }
929 }
930}
931
932QMenu *TodoView::createCategoryPopupMenu()
933{
934 auto tempMenu = new QMenu(this);
935
936 QModelIndexList selection = mView->selectionModel()->selectedRows();
937 if (selection.size() != 1) {
938 return tempMenu;
939 }
940
941 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
943 Q_ASSERT(todo);
944
945 const QStringList checkedCategories = todo->categories();
946
947 auto tagFetchJob = new Akonadi::TagFetchJob(this);
948 connect(tagFetchJob, &Akonadi::TagFetchJob::result, this, &TodoView::onTagsFetched);
949 tagFetchJob->setProperty("menu", QVariant::fromValue(QPointer<QMenu>(tempMenu)));
950 tagFetchJob->setProperty("checkedCategories", checkedCategories);
951
952 connect(tempMenu, &QMenu::triggered, this, &TodoView::changedCategories);
953 connect(tempMenu, &QMenu::aboutToHide, tempMenu, &QMenu::deleteLater);
954 return tempMenu;
955}
956
957void TodoView::onTagsFetched(KJob *job)
958{
959 if (job->error()) {
960 qCWarning(CALENDARVIEW_LOG) << "Failed to fetch tags " << job->errorString();
961 return;
962 }
963 auto fetchJob = static_cast<Akonadi::TagFetchJob *>(job);
964 const QStringList checkedCategories = job->property("checkedCategories").toStringList();
965 auto menu = job->property("menu").value<QPointer<QMenu>>();
966 if (menu) {
967 const auto lst = fetchJob->tags();
968 for (const Akonadi::Tag &tag : lst) {
969 const QString name = tag.name();
970 QAction *action = menu->addAction(name);
971 action->setCheckable(true);
972 action->setData(name);
973 if (checkedCategories.contains(name)) {
974 action->setChecked(true);
975 }
976 }
977 }
978}
979
980void TodoView::setNewDate(QDate date)
981{
982 QModelIndexList selection = mView->selectionModel()->selectedRows();
983 if (selection.size() != 1) {
984 return;
985 }
986
987 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
989 Q_ASSERT(todo);
990
991 const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
992 if (collection.rights() & Akonadi::Collection::CanChangeItem) {
993 KCalendarCore::Todo::Ptr oldTodo(todo->clone());
994 QDateTime dt(date.startOfDay());
995
996 if (!todo->allDay()) {
997 dt.setTime(todo->dtDue().time());
998 }
999
1000 if (todo->hasStartDate() && dt < todo->dtStart()) {
1001 todo->setDtStart(dt);
1002 }
1003 todo->setDtDue(dt);
1004
1005 changer()->modifyIncidence(todoItem, oldTodo, this);
1006 } else {
1007 qCDebug(CALENDARVIEW_LOG) << "Item is readOnly";
1008 }
1009}
1010
1011void TodoView::setStartDate(QDate date)
1012{
1013 QModelIndexList selection = mView->selectionModel()->selectedRows();
1014 if (selection.size() != 1) {
1015 return;
1016 }
1017
1018 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1020 Q_ASSERT(todo);
1021
1022 const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
1023 if (collection.rights() & Akonadi::Collection::CanChangeItem) {
1024 KCalendarCore::Todo::Ptr oldTodo(todo->clone());
1025 QDateTime dt(date.startOfDay());
1026
1027 if (!todo->allDay()) {
1028 dt.setTime(todo->dtStart().time());
1029 }
1030
1031 if (todo->hasDueDate() && dt > todo->dtDue()) {
1032 todo->setDtDue(dt);
1033 }
1034 todo->setDtStart(dt);
1035
1036 changer()->modifyIncidence(todoItem, oldTodo, this);
1037 } else {
1038 qCDebug(CALENDARVIEW_LOG) << "Item is readOnly";
1039 }
1040}
1041
1042void TodoView::setNewPercentage(QAction *action)
1043{
1044 QModelIndexList selection = mView->selectionModel()->selectedRows();
1045 if (selection.size() != 1) {
1046 return;
1047 }
1048
1049 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1051 Q_ASSERT(todo);
1052
1053 const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
1054 if (collection.rights() & Akonadi::Collection::CanChangeItem) {
1055 KCalendarCore::Todo::Ptr oldTodo(todo->clone());
1056
1057 int percentage = mPercentage.value(action);
1058 if (percentage == 100) {
1059 todo->setCompleted(QDateTime::currentDateTime());
1060 todo->setPercentComplete(100);
1061 } else {
1062 todo->setPercentComplete(percentage);
1063 }
1064 changer()->modifyIncidence(todoItem, oldTodo, this);
1065 } else {
1066 qCDebug(CALENDARVIEW_LOG) << "Item is read only";
1067 }
1068}
1069
1070void TodoView::setNewPriority(QAction *action)
1071{
1072 const QModelIndexList selection = mView->selectionModel()->selectedRows();
1073 if (selection.size() != 1) {
1074 return;
1075 }
1076 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1078 const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
1079 if (collection.rights() & Akonadi::Collection::CanChangeItem) {
1080 KCalendarCore::Todo::Ptr oldTodo(todo->clone());
1081 todo->setPriority(mPriority[action]);
1082
1083 changer()->modifyIncidence(todoItem, oldTodo, this);
1084 }
1085}
1086
1087void TodoView::changedCategories(QAction *action)
1088{
1089 const QModelIndexList selection = mView->selectionModel()->selectedRows();
1090 if (selection.size() != 1) {
1091 return;
1092 }
1093
1094 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1096 Q_ASSERT(todo);
1097 const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
1098 if (collection.rights() & Akonadi::Collection::CanChangeItem) {
1099 KCalendarCore::Todo::Ptr oldTodo(todo->clone());
1100
1101 const QString cat = action->data().toString();
1102 QStringList categories = todo->categories();
1103 if (categories.contains(cat)) {
1104 categories.removeAll(cat);
1105 } else {
1106 categories.append(cat);
1107 }
1108 categories.sort();
1109 todo->setCategories(categories);
1110 changer()->modifyIncidence(todoItem, oldTodo, this);
1111 } else {
1112 qCDebug(CALENDARVIEW_LOG) << "No active item, active item is read-only, or locking failed";
1113 }
1114}
1115
1116void TodoView::setFullView(bool fullView)
1117{
1118 if (!mFullViewButton) {
1119 return;
1120 }
1121
1122 mFullViewButton->setChecked(fullView);
1123 if (fullView) {
1124 mFullViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-restore")));
1125 } else {
1126 mFullViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen")));
1127 }
1128
1129 mFullViewButton->blockSignals(true);
1130 // We block signals to avoid recursion; there are two TodoViews and
1131 // also mFullViewButton is synchronized.
1132 mFullViewButton->setChecked(fullView);
1133 mFullViewButton->blockSignals(false);
1134
1135 preferences()->setFullViewTodo(fullView);
1136 preferences()->writeConfig();
1137
1138 Q_EMIT fullViewChanged(fullView);
1139}
1140
1141void TodoView::setFlatView(bool flatView, bool notifyOtherViews)
1142{
1143 if (flatView) {
1144 mFlatViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-list-tree")));
1145 } else {
1146 mFlatViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-list-details")));
1147 }
1148
1149 if (notifyOtherViews) {
1150 sModels->setFlatView(flatView);
1151 }
1152}
1153
1154void TodoView::onRowsInserted(const QModelIndex &parent, int start, int end)
1155{
1156 if (start != end || !entityTreeModel()) {
1157 return;
1158 }
1159
1160 QModelIndex idx = mView->model()->index(start, 0);
1161
1162 // If the collection is currently being populated, we don't do anything
1163 QVariant v = idx.data(Akonadi::EntityTreeModel::ItemRole);
1164 if (!v.isValid()) {
1165 return;
1166 }
1167
1168 auto item = v.value<Akonadi::Item>();
1169 if (!item.isValid()) {
1170 return;
1171 }
1172
1173 const bool isPopulated = entityTreeModel()->isCollectionPopulated(item.storageCollectionId());
1174 if (!isPopulated) {
1175 return;
1176 }
1177
1178 // Case #1, adding an item that doesn't have parent: We select it
1179 if (!parent.isValid()) {
1180 QModelIndexList selection = mView->selectionModel()->selectedRows();
1181 if (selection.size() <= 1) {
1182 // don't destroy complex selections, not applicable now (only single
1183 // selection allowed), but for the future...
1184 int colCount = static_cast<int>(Akonadi::TodoModel::ColumnCount);
1185 mView->selectionModel()->select(QItemSelection(idx, mView->model()->index(start, colCount - 1)),
1187 }
1188 return;
1189 }
1190
1191 // Case 2: Adding an item that has a parent: we expand the parent
1192 if (sModels->isFlatView()) {
1193 return;
1194 }
1195
1196 QModelIndex index = parent;
1197 mView->expand(index);
1198 while (index.parent().isValid()) {
1199 mView->expand(index.parent());
1200 index = index.parent();
1201 }
1202}
1203
1204void TodoView::getHighlightMode(bool &highlightEvents, bool &highlightTodos, bool &highlightJournals)
1205{
1206 highlightTodos = preferences()->highlightTodos();
1207 highlightEvents = !highlightTodos;
1208 highlightJournals = false;
1209}
1210
1211bool TodoView::usesFullWindow()
1212{
1213 return preferences()->fullViewTodo();
1214}
1215
1216void TodoView::resizeColumns()
1217{
1218 mResizeColumnsScheduled = false;
1219
1220 mView->resizeColumnToContents(Akonadi::TodoModel::StartDateColumn);
1221 mView->resizeColumnToContents(Akonadi::TodoModel::DueDateColumn);
1222 mView->resizeColumnToContents(Akonadi::TodoModel::CompletedDateColumn);
1223 mView->resizeColumnToContents(Akonadi::TodoModel::PriorityColumn);
1224 mView->resizeColumnToContents(Akonadi::TodoModel::CalendarColumn);
1225 mView->resizeColumnToContents(Akonadi::TodoModel::RecurColumn);
1226 mView->resizeColumnToContents(Akonadi::TodoModel::PercentColumn);
1227
1228 // We have 3 columns that should stretch: summary, description and categories.
1229 // Summary is always visible.
1230 const bool descriptionVisible = !mView->isColumnHidden(Akonadi::TodoModel::DescriptionColumn);
1231 const bool categoriesVisible = !mView->isColumnHidden(Akonadi::TodoModel::CategoriesColumn);
1232
1233 // Calculate size of non-stretchable columns:
1234 int size = 0;
1235 for (int i = 0; i < Akonadi::TodoModel::ColumnCount; ++i) {
1236 if (!mView->isColumnHidden(i) && i != Akonadi::TodoModel::SummaryColumn && i != Akonadi::TodoModel::DescriptionColumn
1237 && i != Akonadi::TodoModel::CategoriesColumn) {
1238 size += mView->columnWidth(i);
1239 }
1240 }
1241
1242 // Calculate the remaining space that we have for the stretchable columns
1243 int remainingSize = mView->header()->width() - size;
1244
1245 // 100 for summary, 100 for description
1246 const int requiredSize = descriptionVisible ? 200 : 100;
1247
1248 if (categoriesVisible) {
1249 const int categorySize = 100;
1250 mView->setColumnWidth(Akonadi::TodoModel::CategoriesColumn, categorySize);
1251 remainingSize -= categorySize;
1252 }
1253
1254 if (remainingSize < requiredSize) {
1255 // Too little size, so let's use a horizontal scrollbar
1256 mView->resizeColumnToContents(Akonadi::TodoModel::SummaryColumn);
1257 mView->resizeColumnToContents(Akonadi::TodoModel::DescriptionColumn);
1258 } else if (descriptionVisible) {
1259 mView->setColumnWidth(Akonadi::TodoModel::SummaryColumn, remainingSize / 2);
1260 mView->setColumnWidth(Akonadi::TodoModel::DescriptionColumn, remainingSize / 2);
1261 } else {
1262 mView->setColumnWidth(Akonadi::TodoModel::SummaryColumn, remainingSize);
1263 }
1264}
1265
1266void TodoView::restoreViewState()
1267{
1268 if (sModels->isFlatView()) {
1269 return;
1270 }
1271
1272 if (sModels->todoTreeModel && !sModels->todoTreeModel->sourceModel()) {
1273 return;
1274 }
1275
1276 // QElapsedTimer timer;
1277 // timer.start();
1278 delete mTreeStateRestorer;
1279 mTreeStateRestorer = new Akonadi::ETMViewStateSaver();
1280 KSharedConfig::Ptr config = KSharedConfig::openConfig();
1281 KConfigGroup group(config, stateSaverGroup());
1282 mTreeStateRestorer->setView(mView);
1283 mTreeStateRestorer->restoreState(group);
1284 // qCDebug(CALENDARVIEW_LOG) << "Took " << timer.elapsed();
1285}
1286
1287QString TodoView::stateSaverGroup() const
1288{
1289 QString str = QStringLiteral("TodoTreeViewState");
1290 if (mSidebarView) {
1291 str += QLatin1Char('S');
1292 }
1293
1294 return str;
1295}
1296
1297void TodoView::saveViewState()
1298{
1299 Akonadi::ETMViewStateSaver treeStateSaver;
1300 KConfigGroup group(preferences()->config(), stateSaverGroup());
1301 treeStateSaver.setView(mView);
1302 treeStateSaver.saveState(group);
1303}
1304
1305void TodoView::resizeEvent(QResizeEvent *event)
1306{
1308 scheduleResizeColumns();
1309}
1310
1311void TodoView::createEvent()
1312{
1313 const QModelIndexList selection = mView->selectionModel()->selectedRows();
1314 if (selection.size() != 1) {
1315 return;
1316 }
1317
1318 const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1319
1320 Q_EMIT createEvent(todoItem);
1321}
1322
1323#include "todoview.moc"
1324
1325#include "moc_todoview.cpp"
Rights rights() const
static Collection updatedCollection(const QAbstractItemModel *model, qint64 collectionId)
void indexChangedParent(const QModelIndex &index)
Collection::Id storageCollectionId() const
bool isValid() const
QList< Item > List
EventView is the abstract base class from which all other calendar views for event data are derived.
Definition eventview.h:69
void showIncidenceSignal(const Akonadi::Item &)
instructs the receiver to show the incidence in read-only mode.
void deleteIncidenceSignal(const Akonadi::Item &)
instructs the receiver to delete the Incidence in some manner; some possibilities include automatical...
void editIncidenceSignal(const Akonadi::Item &)
instructs the receiver to begin editing the incidence specified in some manner.
virtual void setIncidenceChanger(Akonadi::IncidenceChanger *changer)
Assign a new incidence change helper object.
void getHighlightMode(bool &highlightEvents, bool &highlightTodos, bool &highlightJournals)
documentation in baseview.h
Akonadi::Item::List selectedIncidences() const override
Definition todoview.cpp:526
KCalendarCore::DateList selectedIncidenceDates() const override
Returns a list of the dates of selected events.
Definition todoview.cpp:537
static QString createUniqueId()
QSharedPointer< Calendar > Ptr
QSharedPointer< Incidence > Ptr
QSharedPointer< Todo > Ptr
KConfigGroup group(const QString &group)
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
QString readEntry(const char *key, const char *aDefault=nullptr) const
void dateChanged(const QDate &date)
virtual QString errorString() const
int error() const
void result(KJob *job)
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
This delegate is responsible for displaying the categories of todos.
This delegate is responsible for displaying progress bars for the completion status of indivitual tod...
This delegate is responsible for displaying the due date of todos.
This delegate is responsible for displaying the priority of todos.
This delegate is responsible for displaying possible rich text elements of a todo.
Q_SCRIPTABLE Q_NOREPLY void start()
QString xi18nc(const char *context, const char *text, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Incidence::Ptr incidence(const Akonadi::Item &item)
AKONADI_CALENDAR_EXPORT KCalendarCore::Todo::Ptr todo(const Akonadi::Item &item)
QString fullName(const PartType &type)
Namespace EventViews provides facilities for displaying incidences, including events,...
Definition agenda.h:33
QList< QDate > DateList
QAction * preferences(const QObject *recvr, const char *slot, QObject *parent)
QString name(StandardAction id)
QString label(StandardShortcut id)
void setChecked(bool)
void toggled(bool checked)
QAbstractItemModel(QObject *parent)
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const=0
virtual QModelIndex parent(const QModelIndex &index) const const=0
void rowsInserted(const QModelIndex &parent, int first, int last)
void doubleClicked(const QModelIndex &index)
void setDragDropMode(DragDropMode behavior)
void setCheckable(bool)
void setChecked(bool)
QVariant data() const const
void setData(const QVariant &data)
QDate currentDate()
QDateTime startOfDay() const const
QDateTime currentDateTime()
void setDate(QDate date)
int count() const const
void geometriesChanged()
void moveSection(int from, int to)
void resizeSection(int logicalIndex, int size)
int sectionSize(int logicalIndex) const const
bool isSortIndicatorShown() const const
Qt::SortOrder sortIndicatorOrder() const const
int sortIndicatorSection() const const
int visualIndex(int logicalIndex) const const
QIcon fromTheme(const QString &name)
virtual QModelIndex mapFromSource(const QModelIndex &sourceIndex) const const override
void currentChanged(const QModelIndex &current, const QModelIndex &previous)
void append(QList< T > &&value)
pointer data()
T & first()
bool isEmpty() const const
qsizetype removeAll(const AT &t)
void reserve(qsizetype size)
void aboutToHide()
void triggered(QAction *action)
QVariant data(int role) const const
Qt::ItemFlags flags() const const
bool isValid() const const
const QAbstractItemModel * model() const const
QModelIndex parent() const const
int row() const const
QModelIndex sibling(int row, int column) const const
QObject(QObject *parent)
Q_EMITQ_EMIT
Q_OBJECTQ_OBJECT
bool blockSignals(bool block)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
QObject * parent() const const
QVariant property(const char *name) const const
void setObjectName(QAnyStringView name)
QSortFilterProxyModel(QObject *parent)
void setFilterRegularExpression(const QRegularExpression &regularExpression)
virtual void setSourceModel(QAbstractItemModel *sourceModel) override
QString arg(Args &&... args) const const
bool isEmpty() const const
QString trimmed() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
void sort(Qt::CaseSensitivity cs)
CaseInsensitive
CustomContextMenu
EditRole
ItemIsEditable
typedef KeyboardModifiers
AscendingOrder
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
int columnWidth(int column) const const
QHeaderView * header() const const
bool isColumnHidden(int column) const const
void resizeColumnToContents(int column)
void setRootIsDecorated(bool show)
QVariant fromValue(T &&value)
bool isValid() const const
QString toString() const const
QStringList toStringList() const const
T value() const const
void customContextMenuRequested(const QPoint &pos)
virtual bool event(QEvent *event) override
void hide()
virtual void resizeEvent(QResizeEvent *event)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 4 2025 12:03:37 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.