File indexing completed on 2024-06-16 04:51:16

0001 /*
0002   This file is part of KOrganizer.
0003 
0004   SPDX-FileCopyrightText: 2000, 2001, 2003 Cornelius Schumacher <schumacher@kde.org>
0005   SPDX-FileCopyrightText: 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0006   SPDX-FileCopyrightText: 2005 Rafal Rzepecki <divide@users.sourceforge.net>
0007   SPDX-FileCopyrightText: 2008 Thomas Thrainer <tom_t@gmx.at>
0008   SPDX-FileCopyrightText: 2013 Sérgio Martins <iamsergio@gmail.com>
0009 
0010   SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0011 */
0012 
0013 #include "todoview.h"
0014 
0015 #include "calendarview_debug.h"
0016 #include "coloredtodoproxymodel.h"
0017 #include "tododelegates.h"
0018 #include "todoviewquickaddline.h"
0019 #include "todoviewquicksearch.h"
0020 #include "todoviewsortfilterproxymodel.h"
0021 #include "todoviewview.h"
0022 
0023 #include <Akonadi/CalendarUtils>
0024 #include <Akonadi/EntityMimeTypeFilterModel>
0025 #include <Akonadi/EntityTreeModel>
0026 #include <Akonadi/TagFetchJob>
0027 
0028 #include <Akonadi/ETMViewStateSaver>
0029 #include <Akonadi/IncidenceTreeModel>
0030 #include <Akonadi/TodoModel>
0031 
0032 #include <CalendarSupport/KCalPrefs>
0033 
0034 #include <KCalendarCore/CalFormat>
0035 
0036 #include <KConfig>
0037 #include <KDatePickerPopup>
0038 #include <KDescendantsProxyModel>
0039 #include <KJob>
0040 #include <KMessageBox>
0041 
0042 #include <QGridLayout>
0043 #include <QHeaderView>
0044 #include <QIcon>
0045 #include <QMenu>
0046 #include <QSortFilterProxyModel>
0047 #include <QToolButton>
0048 
0049 #include <chrono>
0050 
0051 using namespace std::chrono_literals;
0052 
0053 Q_DECLARE_METATYPE(QPointer<QMenu>)
0054 
0055 using namespace EventViews;
0056 using namespace KCalendarCore;
0057 
0058 namespace EventViews
0059 {
0060 
0061 class CalendarFilterModel : public QSortFilterProxyModel
0062 {
0063     Q_OBJECT
0064 public:
0065     explicit CalendarFilterModel(QObject *parent = nullptr)
0066         : QSortFilterProxyModel(parent)
0067     {
0068         mDescendantsProxy.setDisplayAncestorData(false);
0069         QSortFilterProxyModel::setSourceModel(&mDescendantsProxy);
0070     }
0071 
0072     void setSourceModel(QAbstractItemModel *model) override
0073     {
0074         mDescendantsProxy.setSourceModel(model);
0075     }
0076 
0077     bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
0078     {
0079         const auto source_index = sourceModel()->index(source_row, 0, source_parent);
0080         const auto item = sourceModel()->data(source_index, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0081 
0082         if (!item.isValid()) {
0083             return false;
0084         }
0085         return mEnabledCalendars.contains(item.parentCollection().id());
0086     }
0087 
0088     void addCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
0089     {
0090         mEnabledCalendars.insert(calendar->collection().id());
0091         invalidateFilter();
0092     }
0093 
0094     void removeCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
0095     {
0096         mEnabledCalendars.remove(calendar->collection().id());
0097         invalidateFilter();
0098     }
0099 
0100 private:
0101     KDescendantsProxyModel mDescendantsProxy;
0102     QSet<Akonadi::Collection::Id> mEnabledCalendars;
0103 };
0104 
0105 // We share this struct between all views, for performance and memory purposes
0106 class ModelStack
0107 {
0108 public:
0109     ModelStack(const EventViews::PrefsPtr &preferences, QObject *parent_)
0110         : todoModel(new Akonadi::TodoModel())
0111         , coloredTodoModel(new ColoredTodoProxyModel(preferences))
0112         , parent(parent_)
0113         , prefs(preferences)
0114     {
0115         coloredTodoModel->setSourceModel(todoModel);
0116     }
0117 
0118     ~ModelStack()
0119     {
0120         delete coloredTodoModel;
0121         delete todoModel;
0122         delete todoTreeModel;
0123         delete todoFlatModel;
0124     }
0125 
0126     void registerView(TodoView *view)
0127     {
0128         views << view;
0129     }
0130 
0131     void unregisterView(TodoView *view)
0132     {
0133         views.removeAll(view);
0134     }
0135 
0136     void setFlatView(bool flat)
0137     {
0138         const QString todoMimeType = QStringLiteral("application/x-vnd.akonadi.calendar.todo");
0139         if (flat) {
0140             for (TodoView *view : std::as_const(views)) {
0141                 // In flatview dropping confuses users and it's very easy to drop into a child item
0142                 view->mView->setDragDropMode(QAbstractItemView::DragOnly);
0143                 view->setFlatView(flat, /**propagate=*/false); // So other views update their toggle icon
0144 
0145                 if (todoTreeModel) {
0146                     view->saveViewState(); // Save the tree state before it's gone
0147                 }
0148             }
0149 
0150             delete todoFlatModel;
0151             todoFlatModel = new Akonadi::EntityMimeTypeFilterModel(parent);
0152             todoFlatModel->addMimeTypeInclusionFilter(todoMimeType);
0153             todoFlatModel->setSourceModel(model);
0154             todoModel->setSourceModel(todoFlatModel);
0155 
0156             delete todoTreeModel;
0157             todoTreeModel = nullptr;
0158         } else {
0159             delete todoTreeModel;
0160             todoTreeModel = new Akonadi::IncidenceTreeModel(QStringList() << todoMimeType, parent);
0161             for (TodoView *view : std::as_const(views)) {
0162                 QObject::connect(todoTreeModel, &Akonadi::IncidenceTreeModel::indexChangedParent, view, &TodoView::expandIndex);
0163                 QObject::connect(todoTreeModel, &Akonadi::IncidenceTreeModel::batchInsertionFinished, view, &TodoView::restoreViewState);
0164                 view->mView->setDragDropMode(QAbstractItemView::DragDrop);
0165                 view->setFlatView(flat, /**propagate=*/false); // So other views update their toggle icon
0166             }
0167             todoTreeModel->setSourceModel(model);
0168             todoModel->setSourceModel(todoTreeModel);
0169             delete todoFlatModel;
0170             todoFlatModel = nullptr;
0171         }
0172 
0173         for (TodoView *view : std::as_const(views)) {
0174             view->mFlatViewButton->blockSignals(true);
0175             // We block signals to avoid recursion, we have two TodoViews and mFlatViewButton is synchronized
0176             view->mFlatViewButton->setChecked(flat);
0177             view->mFlatViewButton->blockSignals(false);
0178             view->mView->setRootIsDecorated(!flat);
0179             view->restoreViewState();
0180         }
0181 
0182         prefs->setFlatListTodo(flat);
0183         prefs->writeConfig();
0184     }
0185 
0186     void setModel(QAbstractItemModel *model)
0187     {
0188         this->model = model;
0189         if (todoTreeModel) {
0190             todoTreeModel->setSourceModel(this->model);
0191         }
0192     }
0193 
0194     bool isFlatView() const
0195     {
0196         return todoFlatModel != nullptr;
0197     }
0198 
0199     Akonadi::TodoModel *const todoModel;
0200     ColoredTodoProxyModel *const coloredTodoModel;
0201     QList<TodoView *> views;
0202     QObject *parent = nullptr;
0203 
0204     QAbstractItemModel *model = nullptr;
0205     Akonadi::IncidenceTreeModel *todoTreeModel = nullptr;
0206     Akonadi::EntityMimeTypeFilterModel *todoFlatModel = nullptr;
0207     EventViews::PrefsPtr prefs;
0208 };
0209 }
0210 
0211 // Don't use K_GLOBAL_STATIC, see QTBUG-22667
0212 static ModelStack *sModels = nullptr;
0213 
0214 TodoView::TodoView(const EventViews::PrefsPtr &prefs, bool sidebarView, QWidget *parent)
0215     : EventView(parent)
0216     , mCalendarFilterModel(std::make_unique<CalendarFilterModel>())
0217     , mQuickSearch(nullptr)
0218     , mQuickAdd(nullptr)
0219     , mTreeStateRestorer(nullptr)
0220     , mSidebarView(sidebarView)
0221     , mResizeColumnsScheduled(false)
0222 {
0223     mResizeColumnsTimer = new QTimer(this);
0224     connect(mResizeColumnsTimer, &QTimer::timeout, this, &TodoView::resizeColumns);
0225     mResizeColumnsTimer->setInterval(100ms); // so we don't overdue it when user resizes window manually
0226     mResizeColumnsTimer->setSingleShot(true);
0227 
0228     setPreferences(prefs);
0229     if (!sModels) {
0230         sModels = new ModelStack(prefs, parent);
0231         connect(sModels->todoModel, &Akonadi::TodoModel::dropOnSelfRejected, this, []() {
0232             KMessageBox::information(nullptr,
0233                                      i18n("Cannot move to-do to itself or a child of itself."),
0234                                      i18nc("@title:window", "Drop To-do"),
0235                                      QStringLiteral("NoDropTodoOntoItself"));
0236         });
0237     }
0238     sModels->registerView(this);
0239     sModels->setModel(mCalendarFilterModel.get());
0240 
0241     mProxyModel = new TodoViewSortFilterProxyModel(preferences(), this);
0242     mProxyModel->setSourceModel(sModels->coloredTodoModel);
0243     mProxyModel->setDynamicSortFilter(true);
0244     mProxyModel->setFilterKeyColumn(Akonadi::TodoModel::SummaryColumn);
0245     mProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0246     mProxyModel->setSortRole(Qt::EditRole);
0247     connect(mProxyModel, &TodoViewSortFilterProxyModel::rowsInserted, this, &TodoView::onRowsInserted);
0248 
0249     if (!mSidebarView) {
0250         mQuickSearch = new TodoViewQuickSearch(this);
0251         mQuickSearch->setVisible(prefs->enableTodoQuickSearch());
0252         connect(mQuickSearch,
0253                 &TodoViewQuickSearch::searchTextChanged,
0254                 mProxyModel,
0255                 qOverload<const QString &>(&QSortFilterProxyModel::setFilterRegularExpression));
0256         connect(mQuickSearch, &TodoViewQuickSearch::searchTextChanged, this, &TodoView::restoreViewState);
0257         connect(mQuickSearch, &TodoViewQuickSearch::filterCategoryChanged, mProxyModel, &TodoViewSortFilterProxyModel::setCategoryFilter);
0258         connect(mQuickSearch, &TodoViewQuickSearch::filterCategoryChanged, this, &TodoView::restoreViewState);
0259         connect(mQuickSearch, &TodoViewQuickSearch::filterPriorityChanged, mProxyModel, &TodoViewSortFilterProxyModel::setPriorityFilter);
0260         connect(mQuickSearch, &TodoViewQuickSearch::filterPriorityChanged, this, &TodoView::restoreViewState);
0261     }
0262 
0263     mView = new TodoViewView(this);
0264     mView->setModel(mProxyModel);
0265 
0266     mView->setContextMenuPolicy(Qt::CustomContextMenu);
0267 
0268     mView->setSortingEnabled(true);
0269 
0270     mView->setAutoExpandDelay(250);
0271     mView->setDragDropMode(QAbstractItemView::DragDrop);
0272 
0273     mView->setExpandsOnDoubleClick(false);
0274     mView->setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed);
0275 
0276     connect(mView->header(), &QHeaderView::geometriesChanged, this, &TodoView::scheduleResizeColumns);
0277     connect(mView, &TodoViewView::visibleColumnCountChanged, this, &TodoView::resizeColumns);
0278 
0279     auto richTextDelegate = new TodoRichTextDelegate(mView);
0280     mView->setItemDelegateForColumn(Akonadi::TodoModel::SummaryColumn, richTextDelegate);
0281     mView->setItemDelegateForColumn(Akonadi::TodoModel::DescriptionColumn, richTextDelegate);
0282 
0283     auto priorityDelegate = new TodoPriorityDelegate(mView);
0284     mView->setItemDelegateForColumn(Akonadi::TodoModel::PriorityColumn, priorityDelegate);
0285 
0286     auto startDateDelegate = new TodoDueDateDelegate(mView);
0287     mView->setItemDelegateForColumn(Akonadi::TodoModel::StartDateColumn, startDateDelegate);
0288 
0289     auto dueDateDelegate = new TodoDueDateDelegate(mView);
0290     mView->setItemDelegateForColumn(Akonadi::TodoModel::DueDateColumn, dueDateDelegate);
0291 
0292     auto completeDelegate = new TodoCompleteDelegate(mView);
0293     mView->setItemDelegateForColumn(Akonadi::TodoModel::PercentColumn, completeDelegate);
0294 
0295     mCategoriesDelegate = new TodoCategoriesDelegate(mView);
0296     mView->setItemDelegateForColumn(Akonadi::TodoModel::CategoriesColumn, mCategoriesDelegate);
0297 
0298     connect(mView, &TodoViewView::customContextMenuRequested, this, &TodoView::contextMenu);
0299     connect(mView, &TodoViewView::doubleClicked, this, &TodoView::itemDoubleClicked);
0300 
0301     connect(mView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TodoView::selectionChanged);
0302 
0303     mQuickAdd = new TodoViewQuickAddLine(this);
0304     mQuickAdd->setClearButtonEnabled(true);
0305     mQuickAdd->setVisible(preferences()->enableQuickTodo());
0306     connect(mQuickAdd, &TodoViewQuickAddLine::returnPressed, this, &TodoView::addQuickTodo);
0307 
0308     mFullViewButton = nullptr;
0309     if (!mSidebarView) {
0310         mFullViewButton = new QToolButton(this);
0311         mFullViewButton->setAutoRaise(true);
0312         mFullViewButton->setCheckable(true);
0313 
0314         mFullViewButton->setToolTip(i18nc("@info:tooltip", "Display to-do list in a full window"));
0315         mFullViewButton->setWhatsThis(i18nc("@info:whatsthis", "Checking this option will cause the to-do view to use the full window."));
0316     }
0317     mFlatViewButton = new QToolButton(this);
0318     mFlatViewButton->setAutoRaise(true);
0319     mFlatViewButton->setCheckable(true);
0320     mFlatViewButton->setToolTip(i18nc("@info:tooltip", "Display to-dos in flat list instead of a tree"));
0321     mFlatViewButton->setWhatsThis(i18nc("@info:whatsthis",
0322                                         "Checking this option will cause the to-dos to be displayed as a "
0323                                         "flat list instead of a hierarchical tree; the parental "
0324                                         "relationships are removed in the display."));
0325 
0326     connect(mFlatViewButton, &QToolButton::toggled, this, [this](bool flatView) {
0327         setFlatView(flatView, true);
0328     });
0329     if (mFullViewButton) {
0330         connect(mFullViewButton, &QToolButton::toggled, this, &TodoView::setFullView);
0331     }
0332 
0333     auto layout = new QGridLayout(this);
0334     layout->setContentsMargins(0, 0, 0, 0);
0335     if (!mSidebarView) {
0336         layout->addWidget(mQuickSearch, 0, 0, 1, 2);
0337     }
0338     layout->addWidget(mView, 1, 0, 1, 2);
0339     layout->setRowStretch(1, 1);
0340     layout->addWidget(mQuickAdd, 2, 0);
0341 
0342     // Dummy layout just to add a few px of right margin so the checkbox is aligned
0343     // with the QAbstractItemView's viewport.
0344     auto dummyLayout = new QHBoxLayout();
0345     dummyLayout->setContentsMargins(0, 0, mView->frameWidth() /*right*/, 0);
0346     if (!mSidebarView) {
0347         auto f = new QFrame(this);
0348         f->setFrameShape(QFrame::VLine);
0349         f->setFrameShadow(QFrame::Sunken);
0350         dummyLayout->addWidget(f);
0351         dummyLayout->addWidget(mFullViewButton);
0352     }
0353     dummyLayout->addWidget(mFlatViewButton);
0354 
0355     layout->addLayout(dummyLayout, 2, 1);
0356 
0357     // ---------------- POPUP-MENUS -----------------------
0358     mItemPopupMenu = new QMenu(this);
0359 
0360     mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("document-preview")),
0361                                                                i18nc("@action:inmenu show the to-do", "&Show"),
0362                                                                this,
0363                                                                &TodoView::showTodo);
0364 
0365     QAction *a = mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("document-edit")),
0366                                            i18nc("@action:inmenu edit the to-do", "&Edit..."),
0367                                            this,
0368                                            &TodoView::editTodo);
0369     mItemPopupMenuReadWriteEntries << a;
0370     mItemPopupMenuItemOnlyEntries << a;
0371 
0372     a = mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")),
0373                                   i18nc("@action:inmenu delete the to-do", "&Delete"),
0374                                   this,
0375                                   &TodoView::deleteTodo);
0376     mItemPopupMenuReadWriteEntries << a;
0377     mItemPopupMenuItemOnlyEntries << a;
0378 
0379     mItemPopupMenu->addSeparator();
0380 
0381     mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("document-print")),
0382                                                                i18nc("@action:inmenu print the to-do", "&Print..."),
0383                                                                this,
0384                                                                &TodoView::printTodo);
0385 
0386     mItemPopupMenuItemOnlyEntries << mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("document-print-preview")),
0387                                                                i18nc("@action:inmenu print preview the to-do", "Print Previe&w..."),
0388                                                                this,
0389                                                                &TodoView::printPreviewTodo);
0390 
0391     mItemPopupMenu->addSeparator();
0392 
0393     mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("view-calendar-tasks")),
0394                               i18nc("@action:inmenu create a new to-do", "New &To-do..."),
0395                               this,
0396                               &TodoView::newTodo);
0397 
0398     a = mItemPopupMenu->addAction(i18nc("@action:inmenu create a new sub-to-do", "New Su&b-to-do..."), this, &TodoView::newSubTodo);
0399     mItemPopupMenuReadWriteEntries << a;
0400     mItemPopupMenuItemOnlyEntries << a;
0401 
0402     mMakeTodoIndependent = mItemPopupMenu->addAction(i18nc("@action:inmenu", "&Make this To-do Independent"), this, &TodoView::unSubTodoSignal);
0403 
0404     mMakeSubtodosIndependent = mItemPopupMenu->addAction(i18nc("@action:inmenu", "Make all Sub-to-dos &Independent"), this, &TodoView::unAllSubTodoSignal);
0405 
0406     mItemPopupMenuItemOnlyEntries << mMakeTodoIndependent;
0407     mItemPopupMenuItemOnlyEntries << mMakeSubtodosIndependent;
0408 
0409     mItemPopupMenuReadWriteEntries << mMakeTodoIndependent;
0410     mItemPopupMenuReadWriteEntries << mMakeSubtodosIndependent;
0411 
0412     mItemPopupMenu->addSeparator();
0413 
0414     a = mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("appointment-new")),
0415                                   i18nc("@action:inmenu", "Create Event from To-do"),
0416                                   this,
0417                                   qOverload<>(&TodoView::createEvent));
0418     a->setObjectName(QLatin1StringView("createevent"));
0419     mItemPopupMenuReadWriteEntries << a;
0420     mItemPopupMenuItemOnlyEntries << a;
0421 
0422     a = mItemPopupMenu->addAction(QIcon::fromTheme(QStringLiteral("view-pim-notes")),
0423                                   i18nc("@action:inmenu", "Create Note for To-do"),
0424                                   this,
0425                                   qOverload<>(&TodoView::createNote));
0426     a->setObjectName(QLatin1StringView("createnote"));
0427     mItemPopupMenuReadWriteEntries << a;
0428     mItemPopupMenuItemOnlyEntries << a;
0429 
0430     mItemPopupMenu->addSeparator();
0431 
0432     mCopyPopupMenu = new KDatePickerPopup(KDatePickerPopup::NoDate | KDatePickerPopup::DatePicker | KDatePickerPopup::Words, QDate::currentDate(), this);
0433     mCopyPopupMenu->setTitle(i18nc("@title:menu", "&Copy To"));
0434 
0435     connect(mCopyPopupMenu, &KDatePickerPopup::dateChanged, this, &TodoView::copyTodoToDate);
0436 
0437     connect(mCopyPopupMenu, &KDatePickerPopup::dateChanged, mItemPopupMenu, &QMenu::hide);
0438 
0439     mMovePopupMenu = new KDatePickerPopup(KDatePickerPopup::NoDate | KDatePickerPopup::DatePicker | KDatePickerPopup::Words, QDate::currentDate(), this);
0440     mMovePopupMenu->setTitle(i18nc("@title:menu", "&Move To"));
0441 
0442     connect(mMovePopupMenu, &KDatePickerPopup::dateChanged, this, &TodoView::setNewDate);
0443     connect(mView->startPopupMenu(), &KDatePickerPopup::dateChanged, this, &TodoView::setStartDate);
0444 
0445     connect(mMovePopupMenu, &KDatePickerPopup::dateChanged, mItemPopupMenu, &QMenu::hide);
0446 
0447     mItemPopupMenu->insertMenu(nullptr, mCopyPopupMenu);
0448     mItemPopupMenu->insertMenu(nullptr, mMovePopupMenu);
0449 
0450     mItemPopupMenu->addSeparator();
0451     mItemPopupMenu->addAction(i18nc("@action:inmenu delete completed to-dos", "Pur&ge Completed"), this, &TodoView::purgeCompletedSignal);
0452 
0453     mPriorityPopupMenu = new QMenu(this);
0454     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu unspecified priority", "unspecified"))] = 0;
0455     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu highest priority", "1 (highest)"))] = 1;
0456     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=2", "2"))] = 2;
0457     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=3", "3"))] = 3;
0458     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=4", "4"))] = 4;
0459     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu medium priority", "5 (medium)"))] = 5;
0460     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=6", "6"))] = 6;
0461     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=7", "7"))] = 7;
0462     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu priority value=8", "8"))] = 8;
0463     mPriority[mPriorityPopupMenu->addAction(i18nc("@action:inmenu lowest priority", "9 (lowest)"))] = 9;
0464     connect(mPriorityPopupMenu, &QMenu::triggered, this, &TodoView::setNewPriority);
0465 
0466     mPercentageCompletedPopupMenu = new QMenu(this);
0467     for (int i = 0; i <= 100; i += 10) {
0468         const QString label = QStringLiteral("%1 %").arg(i);
0469         mPercentage[mPercentageCompletedPopupMenu->addAction(label)] = i;
0470     }
0471     connect(mPercentageCompletedPopupMenu, &QMenu::triggered, this, &TodoView::setNewPercentage);
0472 
0473     setMinimumHeight(50);
0474 
0475     // Initialize our proxy models
0476     setFlatView(preferences()->flatListTodo());
0477     setFullView(preferences()->fullViewTodo());
0478 
0479     updateConfig();
0480 }
0481 
0482 TodoView::~TodoView()
0483 {
0484     saveViewState();
0485 
0486     sModels->unregisterView(this);
0487     if (sModels->views.isEmpty()) {
0488         delete sModels;
0489         sModels = nullptr;
0490     }
0491 }
0492 
0493 void TodoView::expandIndex(const QModelIndex &index)
0494 {
0495     QModelIndex todoModelIndex = sModels->todoModel->mapFromSource(index);
0496     Q_ASSERT(todoModelIndex.isValid());
0497     const auto coloredIndex = sModels->coloredTodoModel->mapFromSource(todoModelIndex);
0498     Q_ASSERT(coloredIndex.isValid());
0499     QModelIndex realIndex = mProxyModel->mapFromSource(coloredIndex);
0500     Q_ASSERT(realIndex.isValid());
0501     while (realIndex.isValid()) {
0502         mView->expand(realIndex);
0503         realIndex = mProxyModel->parent(realIndex);
0504     }
0505 }
0506 
0507 void TodoView::setModel(QAbstractItemModel *model)
0508 {
0509     EventView::setModel(model);
0510 
0511     mCalendarFilterModel->setSourceModel(model);
0512     restoreViewState();
0513 }
0514 
0515 void TodoView::addCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
0516 {
0517     EventView::addCalendar(calendar);
0518     mCalendarFilterModel->addCalendar(calendar);
0519 }
0520 
0521 void TodoView::removeCalendar(const Akonadi::CollectionCalendar::Ptr &calendar)
0522 {
0523     mCalendarFilterModel->removeCalendar(calendar);
0524     EventView::removeCalendar(calendar);
0525 }
0526 
0527 Akonadi::Item::List TodoView::selectedIncidences() const
0528 {
0529     Akonadi::Item::List ret;
0530     const QModelIndexList selection = mView->selectionModel()->selectedRows();
0531     ret.reserve(selection.count());
0532     for (const QModelIndex &mi : selection) {
0533         ret << mi.data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
0534     }
0535     return ret;
0536 }
0537 
0538 DateList TodoView::selectedIncidenceDates() const
0539 {
0540     // The todo view only lists todo's. It's probably not a good idea to
0541     // return something about the selected todo here, because it has got
0542     // a couple of dates (creation, due date, completion date), and the
0543     // caller could not figure out what he gets. So just return an empty list.
0544     return {};
0545 }
0546 
0547 void TodoView::saveLayout(KConfig *config, const QString &group) const
0548 {
0549     KConfigGroup cfgGroup = config->group(group);
0550     QHeaderView *header = mView->header();
0551 
0552     QVariantList columnVisibility;
0553     QVariantList columnOrder;
0554     QVariantList columnWidths;
0555     const int headerCount = header->count();
0556     columnVisibility.reserve(headerCount);
0557     columnWidths.reserve(headerCount);
0558     columnOrder.reserve(headerCount);
0559     for (int i = 0; i < headerCount; ++i) {
0560         columnVisibility << QVariant(!mView->isColumnHidden(i));
0561         columnWidths << QVariant(header->sectionSize(i));
0562         columnOrder << QVariant(header->visualIndex(i));
0563     }
0564     cfgGroup.writeEntry("ColumnVisibility", columnVisibility);
0565     cfgGroup.writeEntry("ColumnOrder", columnOrder);
0566     cfgGroup.writeEntry("ColumnWidths", columnWidths);
0567 
0568     cfgGroup.writeEntry("SortAscending", (int)header->sortIndicatorOrder());
0569     if (header->isSortIndicatorShown()) {
0570         cfgGroup.writeEntry("SortColumn", header->sortIndicatorSection());
0571     } else {
0572         cfgGroup.writeEntry("SortColumn", -1);
0573     }
0574 
0575     if (!mSidebarView) {
0576         preferences()->setFullViewTodo(mFullViewButton->isChecked());
0577     }
0578     preferences()->setFlatListTodo(mFlatViewButton->isChecked());
0579 }
0580 
0581 void TodoView::restoreLayout(KConfig *config, const QString &group, bool minimalDefaults)
0582 {
0583     KConfigGroup cfgGroup = config->group(group);
0584     QHeaderView *header = mView->header();
0585 
0586     QVariantList columnVisibility = cfgGroup.readEntry("ColumnVisibility", QVariantList());
0587     QVariantList columnOrder = cfgGroup.readEntry("ColumnOrder", QVariantList());
0588     QVariantList columnWidths = cfgGroup.readEntry("ColumnWidths", QVariantList());
0589 
0590     if (columnVisibility.isEmpty()) {
0591         // if config is empty then use default settings
0592         mView->hideColumn(Akonadi::TodoModel::RecurColumn);
0593         mView->hideColumn(Akonadi::TodoModel::DescriptionColumn);
0594         mView->hideColumn(Akonadi::TodoModel::CalendarColumn);
0595         mView->hideColumn(Akonadi::TodoModel::CompletedDateColumn);
0596 
0597         if (minimalDefaults) {
0598             mView->hideColumn(Akonadi::TodoModel::PriorityColumn);
0599             mView->hideColumn(Akonadi::TodoModel::PercentColumn);
0600             mView->hideColumn(Akonadi::TodoModel::DescriptionColumn);
0601             mView->hideColumn(Akonadi::TodoModel::CategoriesColumn);
0602         }
0603 
0604         // We don't have any incidences (content) yet, so we delay resizing
0605         QTimer::singleShot(0, this, &TodoView::resizeColumns);
0606     } else {
0607         for (int i = 0; i < header->count() && i < columnOrder.size() && i < columnWidths.size() && i < columnVisibility.size(); i++) {
0608             bool visible = columnVisibility[i].toBool();
0609             int width = columnWidths[i].toInt();
0610             int order = columnOrder[i].toInt();
0611 
0612             header->resizeSection(i, width);
0613             header->moveSection(header->visualIndex(i), order);
0614             if (i != 0 && !visible) {
0615                 mView->hideColumn(i);
0616             }
0617         }
0618     }
0619 
0620     int sortOrder = cfgGroup.readEntry("SortAscending", (int)Qt::AscendingOrder);
0621     int sortColumn = cfgGroup.readEntry("SortColumn", -1);
0622     if (sortColumn >= 0) {
0623         mView->sortByColumn(sortColumn, (Qt::SortOrder)sortOrder);
0624     }
0625 
0626     mFlatViewButton->setChecked(cfgGroup.readEntry("FlatView", false));
0627 }
0628 
0629 void TodoView::setIncidenceChanger(Akonadi::IncidenceChanger *changer)
0630 {
0631     EventView::setIncidenceChanger(changer);
0632     sModels->todoModel->setIncidenceChanger(changer);
0633 }
0634 
0635 void TodoView::showDates(const QDate &start, const QDate &end, const QDate &)
0636 {
0637     // There is nothing to do here for the Todo View
0638     Q_UNUSED(start)
0639     Q_UNUSED(end)
0640 }
0641 
0642 void TodoView::showIncidences(const Akonadi::Item::List &incidenceList, const QDate &date)
0643 {
0644     Q_UNUSED(incidenceList)
0645     Q_UNUSED(date)
0646 }
0647 
0648 void TodoView::updateView()
0649 {
0650     // View is always updated, it's connected to ETM.
0651 }
0652 
0653 void TodoView::changeIncidenceDisplay(const Akonadi::Item &, Akonadi::IncidenceChanger::ChangeType)
0654 {
0655     // Don't do anything, model is connected to ETM, it's up to date
0656 }
0657 
0658 void TodoView::updateConfig()
0659 {
0660     Q_ASSERT(preferences());
0661     if (!mSidebarView && mQuickSearch) {
0662         mQuickSearch->setVisible(preferences()->enableTodoQuickSearch());
0663     }
0664 
0665     if (mQuickAdd) {
0666         mQuickAdd->setVisible(preferences()->enableQuickTodo());
0667     }
0668 
0669     if (mProxyModel) {
0670         mProxyModel->invalidate();
0671     }
0672 
0673     updateView();
0674 }
0675 
0676 void TodoView::clearSelection()
0677 {
0678     mView->selectionModel()->clearSelection();
0679 }
0680 
0681 void TodoView::addTodo(const QString &summary, const Akonadi::Item &parentItem, const QStringList &categories)
0682 {
0683     const QString summaryTrimmed = summary.trimmed();
0684     if (!changer() || summaryTrimmed.isEmpty()) {
0685         return;
0686     }
0687 
0688     KCalendarCore::Todo::Ptr parent = Akonadi::CalendarUtils::todo(parentItem);
0689 
0690     KCalendarCore::Todo::Ptr todo(new KCalendarCore::Todo);
0691     todo->setSummary(summaryTrimmed);
0692     todo->setOrganizer(Person(CalendarSupport::KCalPrefs::instance()->fullName(), CalendarSupport::KCalPrefs::instance()->email()));
0693 
0694     todo->setCategories(categories);
0695 
0696     if (parent && !parent->hasRecurrenceId()) {
0697         todo->setRelatedTo(parent->uid());
0698     }
0699 
0700     Akonadi::Collection collection;
0701 
0702     // Use the same collection of the parent.
0703     if (parentItem.isValid()) {
0704         // Don't use parentColection() since it might be a virtual collection
0705         collection = Akonadi::EntityTreeModel::updatedCollection(model(), parentItem.storageCollectionId());
0706     }
0707 
0708     changer()->createIncidence(todo, collection, this);
0709 }
0710 
0711 void TodoView::addQuickTodo(Qt::KeyboardModifiers modifiers)
0712 {
0713     if (modifiers == Qt::NoModifier) {
0714         /*const QModelIndex index = */
0715         addTodo(mQuickAdd->text(), Akonadi::Item(), mProxyModel->categories());
0716     } else if (modifiers == Qt::ControlModifier) {
0717         QModelIndexList selection = mView->selectionModel()->selectedRows();
0718         if (selection.count() != 1) {
0719             qCWarning(CALENDARVIEW_LOG) << "No to-do selected" << selection;
0720             return;
0721         }
0722         const QModelIndex idx = mProxyModel->mapToSource(selection[0]);
0723         mView->expand(selection[0]);
0724         const auto parent = sModels->coloredTodoModel->data(idx, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0725         addTodo(mQuickAdd->text(), parent, mProxyModel->categories());
0726     } else {
0727         return;
0728     }
0729     mQuickAdd->setText(QString());
0730 }
0731 
0732 void TodoView::contextMenu(QPoint pos)
0733 {
0734     const bool hasItem = mView->indexAt(pos).isValid();
0735     Incidence::Ptr incidencePtr;
0736 
0737     for (QAction *entry : std::as_const(mItemPopupMenuItemOnlyEntries)) {
0738         bool enable;
0739 
0740         if (hasItem) {
0741             const Akonadi::Item::List incidences = selectedIncidences();
0742 
0743             if (incidences.isEmpty()) {
0744                 enable = false;
0745             } else {
0746                 Akonadi::Item item = incidences.first();
0747                 incidencePtr = Akonadi::CalendarUtils::incidence(item);
0748 
0749                 // Action isn't RO, it can change the incidence, "Edit" for example.
0750                 const bool actionIsRw = mItemPopupMenuReadWriteEntries.contains(entry);
0751 
0752                 const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), item.storageCollectionId());
0753                 const bool incidenceIsRO = collection.rights() & Akonadi::Collection::CanChangeItem;
0754 
0755                 enable = hasItem && (!actionIsRw || !incidenceIsRO);
0756             }
0757         } else {
0758             enable = false;
0759         }
0760 
0761         entry->setEnabled(enable);
0762     }
0763     mCopyPopupMenu->setEnabled(hasItem);
0764     mMovePopupMenu->setEnabled(hasItem);
0765 
0766     if (hasItem) {
0767         if (incidencePtr) {
0768             const bool hasRecId = incidencePtr->hasRecurrenceId();
0769             const bool hasSubtodos = mView->model()->hasChildren(mView->indexAt(pos));
0770 
0771             mMakeSubtodosIndependent->setEnabled(!hasRecId && hasSubtodos);
0772             mMakeTodoIndependent->setEnabled(!hasRecId && !incidencePtr->relatedTo().isEmpty());
0773         }
0774 
0775         switch (mView->indexAt(pos).column()) {
0776         case Akonadi::TodoModel::PriorityColumn:
0777             mPriorityPopupMenu->popup(mView->viewport()->mapToGlobal(pos));
0778             break;
0779         case Akonadi::TodoModel::PercentColumn:
0780             mPercentageCompletedPopupMenu->popup(mView->viewport()->mapToGlobal(pos));
0781             break;
0782         case Akonadi::TodoModel::StartDateColumn:
0783             mView->startPopupMenu()->popup(mView->viewport()->mapToGlobal(pos));
0784             break;
0785         case Akonadi::TodoModel::DueDateColumn:
0786             mMovePopupMenu->popup(mView->viewport()->mapToGlobal(pos));
0787             break;
0788             case Akonadi::TodoModel::CategoriesColumn:
0789             createCategoryPopupMenu()->popup(mView->viewport()->mapToGlobal(pos));
0790             break;
0791         default:
0792             mItemPopupMenu->popup(mView->viewport()->mapToGlobal(pos));
0793             break;
0794         }
0795     } else {
0796         mItemPopupMenu->popup(mView->viewport()->mapToGlobal(pos));
0797     }
0798 }
0799 
0800 void TodoView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
0801 {
0802     Q_UNUSED(deselected)
0803     QModelIndexList selection = selected.indexes();
0804     if (selection.isEmpty() || !selection[0].isValid()) {
0805         Q_EMIT incidenceSelected(Akonadi::Item(), QDate());
0806         return;
0807     }
0808 
0809     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
0810 
0811     if (selectedIncidenceDates().isEmpty()) {
0812         Q_EMIT incidenceSelected(todoItem, QDate());
0813     } else {
0814         Q_EMIT incidenceSelected(todoItem, selectedIncidenceDates().at(0));
0815     }
0816 }
0817 
0818 void TodoView::showTodo()
0819 {
0820     QModelIndexList selection = mView->selectionModel()->selectedRows();
0821     if (selection.size() != 1) {
0822         return;
0823     }
0824 
0825     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
0826 
0827     Q_EMIT showIncidenceSignal(todoItem);
0828 }
0829 
0830 void TodoView::editTodo()
0831 {
0832     QModelIndexList selection = mView->selectionModel()->selectedRows();
0833     if (selection.size() != 1) {
0834         return;
0835     }
0836 
0837     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
0838     Q_EMIT editIncidenceSignal(todoItem);
0839 }
0840 
0841 void TodoView::deleteTodo()
0842 {
0843     QModelIndexList selection = mView->selectionModel()->selectedRows();
0844     if (selection.size() == 1) {
0845         const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
0846 
0847         if (!changer()->deletedRecently(todoItem.id())) {
0848             Q_EMIT deleteIncidenceSignal(todoItem);
0849         }
0850     }
0851 }
0852 
0853 void TodoView::newTodo()
0854 {
0855     Q_EMIT newTodoSignal(QDate::currentDate().addDays(7));
0856 }
0857 
0858 void TodoView::newSubTodo()
0859 {
0860     QModelIndexList selection = mView->selectionModel()->selectedRows();
0861     if (selection.size() == 1) {
0862         const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
0863 
0864         Q_EMIT newSubTodoSignal(todoItem);
0865     } else {
0866         // This never happens
0867         qCWarning(CALENDARVIEW_LOG) << "Selection size isn't 1";
0868     }
0869 }
0870 
0871 void TodoView::copyTodoToDate(QDate date)
0872 {
0873     if (!changer()) {
0874         return;
0875     }
0876 
0877     QModelIndexList selection = mView->selectionModel()->selectedRows();
0878     if (selection.size() != 1) {
0879         return;
0880     }
0881 
0882     const QModelIndex origIndex = mProxyModel->mapToSource(selection[0]);
0883     Q_ASSERT(origIndex.isValid());
0884 
0885     const auto origItem = sModels->coloredTodoModel->data(origIndex, Akonadi::EntityTreeModel::ItemRole).value<Akonadi::Item>();
0886 
0887     const KCalendarCore::Todo::Ptr orig = Akonadi::CalendarUtils::todo(origItem);
0888     if (!orig) {
0889         return;
0890     }
0891 
0892     KCalendarCore::Todo::Ptr todo(orig->clone());
0893 
0894     todo->setUid(KCalendarCore::CalFormat::createUniqueId());
0895 
0896     QDateTime due = todo->dtDue();
0897     due.setDate(date);
0898     todo->setDtDue(due);
0899 
0900     changer()->createIncidence(todo, Akonadi::Collection(), this);
0901 }
0902 
0903 void TodoView::scheduleResizeColumns()
0904 {
0905     mResizeColumnsScheduled = true;
0906     mResizeColumnsTimer->start(); // restarts the timer if already active
0907 }
0908 
0909 void TodoView::itemDoubleClicked(const QModelIndex &index)
0910 {
0911     if (index.isValid()) {
0912         QModelIndex summary = index.sibling(index.row(), Akonadi::TodoModel::SummaryColumn);
0913         if (summary.flags() & Qt::ItemIsEditable) {
0914             editTodo();
0915         } else {
0916             showTodo();
0917         }
0918     }
0919 }
0920 
0921 QMenu *TodoView::createCategoryPopupMenu()
0922 {
0923     auto tempMenu = new QMenu(this);
0924 
0925     QModelIndexList selection = mView->selectionModel()->selectedRows();
0926     if (selection.size() != 1) {
0927         return tempMenu;
0928     }
0929 
0930     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
0931     KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(todoItem);
0932     Q_ASSERT(todo);
0933 
0934     const QStringList checkedCategories = todo->categories();
0935 
0936     auto tagFetchJob = new Akonadi::TagFetchJob(this);
0937     connect(tagFetchJob, &Akonadi::TagFetchJob::result, this, &TodoView::onTagsFetched);
0938     tagFetchJob->setProperty("menu", QVariant::fromValue(QPointer<QMenu>(tempMenu)));
0939     tagFetchJob->setProperty("checkedCategories", checkedCategories);
0940 
0941     connect(tempMenu, &QMenu::triggered, this, &TodoView::changedCategories);
0942     connect(tempMenu, &QMenu::aboutToHide, tempMenu, &QMenu::deleteLater);
0943     return tempMenu;
0944 }
0945 
0946 void TodoView::onTagsFetched(KJob *job)
0947 {
0948     if (job->error()) {
0949         qCWarning(CALENDARVIEW_LOG) << "Failed to fetch tags " << job->errorString();
0950         return;
0951     }
0952     auto fetchJob = static_cast<Akonadi::TagFetchJob *>(job);
0953     const QStringList checkedCategories = job->property("checkedCategories").toStringList();
0954     auto menu = job->property("menu").value<QPointer<QMenu>>();
0955     if (menu) {
0956         const auto lst = fetchJob->tags();
0957         for (const Akonadi::Tag &tag : lst) {
0958             const QString name = tag.name();
0959             QAction *action = menu->addAction(name);
0960             action->setCheckable(true);
0961             action->setData(name);
0962             if (checkedCategories.contains(name)) {
0963                 action->setChecked(true);
0964             }
0965         }
0966     }
0967 }
0968 
0969 void TodoView::setNewDate(QDate date)
0970 {
0971     QModelIndexList selection = mView->selectionModel()->selectedRows();
0972     if (selection.size() != 1) {
0973         return;
0974     }
0975 
0976     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
0977     KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(todoItem);
0978     Q_ASSERT(todo);
0979 
0980     const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
0981     if (collection.rights() & Akonadi::Collection::CanChangeItem) {
0982         KCalendarCore::Todo::Ptr oldTodo(todo->clone());
0983         QDateTime dt(date.startOfDay());
0984 
0985         if (!todo->allDay()) {
0986             dt.setTime(todo->dtDue().time());
0987         }
0988 
0989         if (todo->hasStartDate() && dt < todo->dtStart()) {
0990             todo->setDtStart(dt);
0991         }
0992         todo->setDtDue(dt);
0993 
0994         changer()->modifyIncidence(todoItem, oldTodo, this);
0995     } else {
0996         qCDebug(CALENDARVIEW_LOG) << "Item is readOnly";
0997     }
0998 }
0999 
1000 void TodoView::setStartDate(QDate date)
1001 {
1002     QModelIndexList selection = mView->selectionModel()->selectedRows();
1003     if (selection.size() != 1) {
1004         return;
1005     }
1006 
1007     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1008     KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(todoItem);
1009     Q_ASSERT(todo);
1010 
1011     const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
1012     if (collection.rights() & Akonadi::Collection::CanChangeItem) {
1013         KCalendarCore::Todo::Ptr oldTodo(todo->clone());
1014         QDateTime dt(date.startOfDay());
1015 
1016         if (!todo->allDay()) {
1017             dt.setTime(todo->dtStart().time());
1018         }
1019 
1020         if (todo->hasDueDate() && dt > todo->dtDue()) {
1021             todo->setDtDue(dt);
1022         }
1023         todo->setDtStart(dt);
1024 
1025         changer()->modifyIncidence(todoItem, oldTodo, this);
1026     } else {
1027         qCDebug(CALENDARVIEW_LOG) << "Item is readOnly";
1028     }
1029 }
1030 
1031 void TodoView::setNewPercentage(QAction *action)
1032 {
1033     QModelIndexList selection = mView->selectionModel()->selectedRows();
1034     if (selection.size() != 1) {
1035         return;
1036     }
1037 
1038     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1039     KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(todoItem);
1040     Q_ASSERT(todo);
1041 
1042     const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
1043     if (collection.rights() & Akonadi::Collection::CanChangeItem) {
1044         KCalendarCore::Todo::Ptr oldTodo(todo->clone());
1045 
1046         int percentage = mPercentage.value(action);
1047         if (percentage == 100) {
1048             todo->setCompleted(QDateTime::currentDateTime());
1049             todo->setPercentComplete(100);
1050         } else {
1051             todo->setPercentComplete(percentage);
1052         }
1053         changer()->modifyIncidence(todoItem, oldTodo, this);
1054     } else {
1055         qCDebug(CALENDARVIEW_LOG) << "Item is read only";
1056     }
1057 }
1058 
1059 void TodoView::setNewPriority(QAction *action)
1060 {
1061     const QModelIndexList selection = mView->selectionModel()->selectedRows();
1062     if (selection.size() != 1) {
1063         return;
1064     }
1065     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1066     KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(todoItem);
1067     const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
1068     if (collection.rights() & Akonadi::Collection::CanChangeItem) {
1069         KCalendarCore::Todo::Ptr oldTodo(todo->clone());
1070         todo->setPriority(mPriority[action]);
1071 
1072         changer()->modifyIncidence(todoItem, oldTodo, this);
1073     }
1074 }
1075 
1076 void TodoView::changedCategories(QAction *action)
1077 {
1078     const QModelIndexList selection = mView->selectionModel()->selectedRows();
1079     if (selection.size() != 1) {
1080         return;
1081     }
1082 
1083     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1084     KCalendarCore::Todo::Ptr todo = Akonadi::CalendarUtils::todo(todoItem);
1085     Q_ASSERT(todo);
1086     const auto collection = Akonadi::EntityTreeModel::updatedCollection(model(), todoItem.storageCollectionId());
1087     if (collection.rights() & Akonadi::Collection::CanChangeItem) {
1088         KCalendarCore::Todo::Ptr oldTodo(todo->clone());
1089 
1090         const QString cat = action->data().toString();
1091         QStringList categories = todo->categories();
1092         if (categories.contains(cat)) {
1093             categories.removeAll(cat);
1094         } else {
1095             categories.append(cat);
1096         }
1097         categories.sort();
1098         todo->setCategories(categories);
1099         changer()->modifyIncidence(todoItem, oldTodo, this);
1100     } else {
1101         qCDebug(CALENDARVIEW_LOG) << "No active item, active item is read-only, or locking failed";
1102     }
1103 }
1104 
1105 void TodoView::setFullView(bool fullView)
1106 {
1107     if (!mFullViewButton) {
1108         return;
1109     }
1110 
1111     mFullViewButton->setChecked(fullView);
1112     if (fullView) {
1113         mFullViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-restore")));
1114     } else {
1115         mFullViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen")));
1116     }
1117 
1118     mFullViewButton->blockSignals(true);
1119     // We block signals to avoid recursion; there are two TodoViews and
1120     // also mFullViewButton is synchronized.
1121     mFullViewButton->setChecked(fullView);
1122     mFullViewButton->blockSignals(false);
1123 
1124     preferences()->setFullViewTodo(fullView);
1125     preferences()->writeConfig();
1126 
1127     Q_EMIT fullViewChanged(fullView);
1128 }
1129 
1130 void TodoView::setFlatView(bool flatView, bool notifyOtherViews)
1131 {
1132     if (flatView) {
1133         mFlatViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-list-tree")));
1134     } else {
1135         mFlatViewButton->setIcon(QIcon::fromTheme(QStringLiteral("view-list-details")));
1136     }
1137 
1138     if (notifyOtherViews) {
1139         sModels->setFlatView(flatView);
1140     }
1141 }
1142 
1143 void TodoView::onRowsInserted(const QModelIndex &parent, int start, int end)
1144 {
1145     if (start != end || !entityTreeModel()) {
1146         return;
1147     }
1148 
1149     QModelIndex idx = mView->model()->index(start, 0);
1150 
1151     // If the collection is currently being populated, we don't do anything
1152     QVariant v = idx.data(Akonadi::EntityTreeModel::ItemRole);
1153     if (!v.isValid()) {
1154         return;
1155     }
1156 
1157     auto item = v.value<Akonadi::Item>();
1158     if (!item.isValid()) {
1159         return;
1160     }
1161 
1162     const bool isPopulated = entityTreeModel()->isCollectionPopulated(item.storageCollectionId());
1163     if (!isPopulated) {
1164         return;
1165     }
1166 
1167     // Case #1, adding an item that doesn't have parent: We select it
1168     if (!parent.isValid()) {
1169         QModelIndexList selection = mView->selectionModel()->selectedRows();
1170         if (selection.size() <= 1) {
1171             // don't destroy complex selections, not applicable now (only single
1172             // selection allowed), but for the future...
1173             int colCount = static_cast<int>(Akonadi::TodoModel::ColumnCount);
1174             mView->selectionModel()->select(QItemSelection(idx, mView->model()->index(start, colCount - 1)),
1175                                             QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
1176         }
1177         return;
1178     }
1179 
1180     // Case 2: Adding an item that has a parent: we expand the parent
1181     if (sModels->isFlatView()) {
1182         return;
1183     }
1184 
1185     QModelIndex index = parent;
1186     mView->expand(index);
1187     while (index.parent().isValid()) {
1188         mView->expand(index.parent());
1189         index = index.parent();
1190     }
1191 }
1192 
1193 void TodoView::getHighlightMode(bool &highlightEvents, bool &highlightTodos, bool &highlightJournals)
1194 {
1195     highlightTodos = preferences()->highlightTodos();
1196     highlightEvents = !highlightTodos;
1197     highlightJournals = false;
1198 }
1199 
1200 bool TodoView::usesFullWindow()
1201 {
1202     return preferences()->fullViewTodo();
1203 }
1204 
1205 void TodoView::resizeColumns()
1206 {
1207     mResizeColumnsScheduled = false;
1208 
1209     mView->resizeColumnToContents(Akonadi::TodoModel::StartDateColumn);
1210     mView->resizeColumnToContents(Akonadi::TodoModel::DueDateColumn);
1211     mView->resizeColumnToContents(Akonadi::TodoModel::CompletedDateColumn);
1212     mView->resizeColumnToContents(Akonadi::TodoModel::PriorityColumn);
1213     mView->resizeColumnToContents(Akonadi::TodoModel::CalendarColumn);
1214     mView->resizeColumnToContents(Akonadi::TodoModel::RecurColumn);
1215     mView->resizeColumnToContents(Akonadi::TodoModel::PercentColumn);
1216 
1217     // We have 3 columns that should stretch: summary, description and categories.
1218     // Summary is always visible.
1219     const bool descriptionVisible = !mView->isColumnHidden(Akonadi::TodoModel::DescriptionColumn);
1220     const bool categoriesVisible = !mView->isColumnHidden(Akonadi::TodoModel::CategoriesColumn);
1221 
1222     // Calculate size of non-stretchable columns:
1223     int size = 0;
1224     for (int i = 0; i < Akonadi::TodoModel::ColumnCount; ++i) {
1225         if (!mView->isColumnHidden(i) && i != Akonadi::TodoModel::SummaryColumn && i != Akonadi::TodoModel::DescriptionColumn && i != Akonadi::TodoModel::CategoriesColumn) {
1226             size += mView->columnWidth(i);
1227         }
1228     }
1229 
1230     // Calculate the remaining space that we have for the stretchable columns
1231     int remainingSize = mView->header()->width() - size;
1232 
1233     // 100 for summary, 100 for description
1234     const int requiredSize = descriptionVisible ? 200 : 100;
1235 
1236     if (categoriesVisible) {
1237         const int categorySize = 100;
1238         mView->setColumnWidth(Akonadi::TodoModel::CategoriesColumn, categorySize);
1239         remainingSize -= categorySize;
1240     }
1241 
1242     if (remainingSize < requiredSize) {
1243         // Too little size, so let's use a horizontal scrollbar
1244         mView->resizeColumnToContents(Akonadi::TodoModel::SummaryColumn);
1245         mView->resizeColumnToContents(Akonadi::TodoModel::DescriptionColumn);
1246     } else if (descriptionVisible) {
1247         mView->setColumnWidth(Akonadi::TodoModel::SummaryColumn, remainingSize / 2);
1248         mView->setColumnWidth(Akonadi::TodoModel::DescriptionColumn, remainingSize / 2);
1249     } else {
1250         mView->setColumnWidth(Akonadi::TodoModel::SummaryColumn, remainingSize);
1251     }
1252 }
1253 
1254 void TodoView::restoreViewState()
1255 {
1256     if (sModels->isFlatView()) {
1257         return;
1258     }
1259 
1260     if (sModels->todoTreeModel && !sModels->todoTreeModel->sourceModel()) {
1261         return;
1262     }
1263 
1264     // QElapsedTimer timer;
1265     // timer.start();
1266     delete mTreeStateRestorer;
1267     mTreeStateRestorer = new Akonadi::ETMViewStateSaver();
1268     KSharedConfig::Ptr config = KSharedConfig::openConfig();
1269     KConfigGroup group(config, stateSaverGroup());
1270     mTreeStateRestorer->setView(mView);
1271     mTreeStateRestorer->restoreState(group);
1272     // qCDebug(CALENDARVIEW_LOG) << "Took " << timer.elapsed();
1273 }
1274 
1275 QString TodoView::stateSaverGroup() const
1276 {
1277     QString str = QStringLiteral("TodoTreeViewState");
1278     if (mSidebarView) {
1279         str += QLatin1Char('S');
1280     }
1281 
1282     return str;
1283 }
1284 
1285 void TodoView::saveViewState()
1286 {
1287     Akonadi::ETMViewStateSaver treeStateSaver;
1288     KConfigGroup group(preferences()->config(), stateSaverGroup());
1289     treeStateSaver.setView(mView);
1290     treeStateSaver.saveState(group);
1291 }
1292 
1293 void TodoView::resizeEvent(QResizeEvent *event)
1294 {
1295     EventViews::EventView::resizeEvent(event);
1296     scheduleResizeColumns();
1297 }
1298 
1299 void TodoView::createEvent()
1300 {
1301     const QModelIndexList selection = mView->selectionModel()->selectedRows();
1302     if (selection.size() != 1) {
1303         return;
1304     }
1305 
1306     const auto todoItem = selection[0].data(Akonadi::TodoModel::TodoRole).value<Akonadi::Item>();
1307 
1308     Q_EMIT createEvent(todoItem);
1309 }
1310 
1311 void TodoView::createNote()
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 createNote(todoItem);
1321 }
1322 
1323 #include "todoview.moc"
1324 
1325 #include "moc_todoview.cpp"