File indexing completed on 2024-05-19 05:21:54

0001 /*
0002  * Copyright (C) 2003 by Scott Monachello <smonach@cox.net>
0003  * Copyright (C) 2019  Alexander Potashev <aspotashev@gmail.com>
0004  *
0005  *   This program is free software; you can redistribute it and/or modify
0006  *   it under the terms of the GNU General Public License as published by
0007  *   the Free Software Foundation; either version 2 of the License, or
0008  *   (at your option) any later version.
0009  *
0010  *   This program is distributed in the hope that it will be useful,
0011  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013  *   GNU General Public License for more details.
0014  *
0015  *   You should have received a copy of the GNU General Public License along
0016  *   with this program; if not, write to the
0017  *      Free Software Foundation, Inc.
0018  *      51 Franklin Street, Fifth Floor
0019  *      Boston, MA  02110-1301  USA.
0020  *
0021  */
0022 
0023 #include "taskview.h"
0024 
0025 #include <QMouseEvent>
0026 #include <QPointer>
0027 #include <QProgressDialog>
0028 #include <QSortFilterProxyModel>
0029 #include <QTimer>
0030 
0031 #include <KMessageBox>
0032 
0033 #include "desktoptracker.h"
0034 #include "dialogs/edittimedialog.h"
0035 #include "dialogs/exportdialog.h"
0036 #include "dialogs/historydialog.h"
0037 #include "dialogs/taskpropertiesdialog.h"
0038 #include "export/export.h"
0039 #include "focusdetector.h"
0040 #include "idletimedetector.h"
0041 #include "import/plannerparser.h"
0042 #include "ktimetracker.h"
0043 #include "ktimetrackerutility.h"
0044 #include "ktt_debug.h"
0045 #include "model/eventsmodel.h"
0046 #include "model/projectmodel.h"
0047 #include "model/task.h"
0048 #include "model/tasksmodel.h"
0049 #include "treeviewheadercontextmenu.h"
0050 #include "widgets/taskswidget.h"
0051 
0052 void deleteEntry(const QString &key)
0053 {
0054     KConfigGroup config = KSharedConfig::openConfig()->group(QString());
0055     config.deleteEntry(key);
0056     config.sync();
0057 }
0058 
0059 TaskView::TaskView(QWidget *parent)
0060     : QObject(parent)
0061     , m_filterProxyModel(new QSortFilterProxyModel(this))
0062     , m_storage(new TimeTrackerStorage())
0063     , m_focusTrackingActive(false)
0064     , m_lastTaskWithFocus(nullptr)
0065     , m_focusDetector(new FocusDetector())
0066     , m_tasksWidget(nullptr)
0067 {
0068     m_filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0069     m_filterProxyModel->setRecursiveFilteringEnabled(true);
0070     m_filterProxyModel->setSortRole(Task::SortRole);
0071 
0072     connect(m_focusDetector, &FocusDetector::newFocus, this, &TaskView::newFocusWindowDetected);
0073 
0074     // set up the minuteTimer
0075     m_minuteTimer = new QTimer(this);
0076     connect(m_minuteTimer, &QTimer::timeout, this, &TaskView::minuteUpdate);
0077     m_minuteTimer->start(1000 * secsPerMinute);
0078 
0079     // Set up the idle detection.
0080     m_idleTimeDetector = new IdleTimeDetector(KTimeTrackerSettings::period());
0081     connect(m_idleTimeDetector, &IdleTimeDetector::subtractTime, this, &TaskView::subtractTime);
0082     connect(m_idleTimeDetector, &IdleTimeDetector::stopAllTimers, this, &TaskView::stopAllTimers);
0083     if (!IdleTimeDetector::isIdleDetectionPossible()) {
0084         KTimeTrackerSettings::setEnabled(false);
0085     }
0086 
0087     // Setup auto save timer
0088     m_autoSaveTimer = new QTimer(this);
0089     connect(m_autoSaveTimer, &QTimer::timeout, this, &TaskView::save);
0090 
0091     // Connect desktop tracker events to task starting/stopping
0092     m_desktopTracker = new DesktopTracker();
0093     connect(m_desktopTracker, &DesktopTracker::reachedActiveDesktop, this, &TaskView::startTimerForNow);
0094     connect(m_desktopTracker, &DesktopTracker::leftActiveDesktop, this, &TaskView::stopTimerFor);
0095 }
0096 
0097 void TaskView::newFocusWindowDetected(const QString &taskName)
0098 {
0099     QString newTaskName = taskName;
0100     newTaskName.remove(QChar::fromLatin1('\n'));
0101 
0102     if (!m_focusTrackingActive) {
0103         return;
0104     }
0105 
0106     bool found = false; // has taskName been found in our tasks
0107     stopTimerFor(m_lastTaskWithFocus);
0108     for (Task *task : storage()->tasksModel()->getAllTasks()) {
0109         if (task->name() == newTaskName) {
0110             found = true;
0111             startTimerForNow(task);
0112             m_lastTaskWithFocus = task;
0113         }
0114     }
0115     if (!found) {
0116         if (!addTask(newTaskName)) {
0117             KMessageBox::error(nullptr,
0118                                i18n("Error storing new task. Your changes were not saved. "
0119                                     "Make sure you can edit your iCalendar file. "
0120                                     "Also quit all applications using this file and remove "
0121                                     "any lock file related to its name from "
0122                                     "~/.kde/share/apps/kabc/lock/ "));
0123         }
0124         for (Task *task : storage()->tasksModel()->getAllTasks()) {
0125             if (task->name() == newTaskName) {
0126                 startTimerForNow(task);
0127                 m_lastTaskWithFocus = task;
0128             }
0129         }
0130     }
0131     Q_EMIT updateButtons();
0132 }
0133 
0134 TimeTrackerStorage *TaskView::storage()
0135 {
0136     return m_storage;
0137 }
0138 
0139 TaskView::~TaskView()
0140 {
0141     delete m_storage;
0142     KTimeTrackerSettings::self()->save();
0143 }
0144 
0145 void TaskView::load(const QUrl &url)
0146 {
0147     if (m_tasksWidget) {
0148         qFatal("TaskView::load must be called only once");
0149     }
0150 
0151     // if the program is used as an embedded plugin for konqueror, there may be a need
0152     // to load from a file without touching the preferences.
0153     QString err = m_storage->load(this, url);
0154     if (!err.isEmpty()) {
0155         KMessageBox::error(m_tasksWidget, err);
0156         qCDebug(KTT_LOG) << "Leaving TaskView::load";
0157         return;
0158     }
0159 
0160     m_tasksWidget = new TasksWidget(dynamic_cast<QWidget *>(parent()), m_filterProxyModel, nullptr);
0161     connect(m_tasksWidget, &TasksWidget::updateButtons, this, &TaskView::updateButtons);
0162     connect(m_tasksWidget, &TasksWidget::contextMenuRequested, this, &TaskView::contextMenuRequested);
0163     connect(m_tasksWidget, &TasksWidget::taskDoubleClicked, this, &TaskView::onTaskDoubleClicked);
0164     m_tasksWidget->setRootIsDecorated(true);
0165 
0166     reconfigureModel();
0167 
0168     // Connect to the new model created by TimeTrackerStorage::load()
0169     auto *tasksModel = m_storage->tasksModel();
0170     m_filterProxyModel->setSourceModel(tasksModel);
0171     m_tasksWidget->setSourceModel(tasksModel);
0172     m_tasksWidget->reconfigure();
0173     for (int i = 0; i <= tasksModel->columnCount(QModelIndex()); ++i) {
0174         m_tasksWidget->resizeColumnToContents(i);
0175     }
0176 
0177     // Table header context menu
0178     auto *headerContextMenu = new TreeViewHeaderContextMenu(this, m_tasksWidget, QVector<int>{0});
0179     connect(headerContextMenu, &TreeViewHeaderContextMenu::columnToggled, this, &TaskView::slotColumnToggled);
0180 
0181     connect(tasksModel, &TasksModel::taskCompleted, this, &TaskView::stopTimerFor);
0182     connect(tasksModel, &TasksModel::taskDropped, this, &TaskView::reFreshTimes);
0183     connect(tasksModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &TaskView::taskAboutToBeRemoved);
0184     connect(tasksModel, &QAbstractItemModel::rowsRemoved, this, &TaskView::taskRemoved);
0185     connect(storage()->eventsModel(), &EventsModel::timesChanged, this, &TaskView::reFreshTimes);
0186 
0187     // Register tasks with desktop tracker
0188     for (Task *task : storage()->tasksModel()->getAllTasks()) {
0189         m_desktopTracker->registerForDesktops(task, task->desktops());
0190     }
0191 
0192     // Start all tasks that have an event without endtime
0193     for (Task *task : storage()->tasksModel()->getAllTasks()) {
0194         if (!m_storage->allEventsHaveEndTime(task)) {
0195             task->resumeRunning();
0196         }
0197     }
0198     Q_EMIT updateButtons();
0199     Q_EMIT tasksChanged(storage()->tasksModel()->getActiveTasks());
0200 
0201     if (storage()->tasksModel()->getActiveTasks().isEmpty()) {
0202         Q_EMIT timersInactive();
0203     } else {
0204         Q_EMIT timersActive();
0205     }
0206 
0207     if (tasksModel->topLevelItemCount() > 0) {
0208         m_tasksWidget->restoreItemState();
0209         m_tasksWidget->setCurrentIndex(
0210             m_filterProxyModel->mapFromSource(tasksModel->index(tasksModel->topLevelItem(0), 0)));
0211 
0212         if (!m_desktopTracker->startTracking().isEmpty()) {
0213             KMessageBox::error(nullptr,
0214                                i18n("Your virtual desktop number is too high, "
0215                                     "desktop tracking will not work."));
0216         }
0217     }
0218 
0219     storage()->projectModel()->refresh();
0220     tasksWidget()->refresh();
0221 
0222     for (int i = 0; i <= tasksModel->columnCount(QModelIndex()); ++i) {
0223         m_tasksWidget->resizeColumnToContents(i);
0224     }
0225 }
0226 
0227 void TaskView::closeStorage()
0228 {
0229     m_storage->closeStorage();
0230 }
0231 
0232 QString TaskView::reFreshTimes()
0233 {
0234     storage()->projectModel()->refreshTimes();
0235     tasksWidget()->refresh();
0236     return QString();
0237 }
0238 
0239 void TaskView::importPlanner(const QString &fileName)
0240 {
0241     storage()->projectModel()->importPlanner(fileName, m_tasksWidget->currentItem());
0242     tasksWidget()->refresh();
0243 }
0244 
0245 void TaskView::save()
0246 {
0247     qCDebug(KTT_LOG) << "Entering TaskView::save()";
0248     QString err = m_storage->save();
0249 
0250     if (!err.isNull()) {
0251         KMessageBox::error(m_tasksWidget, err);
0252     }
0253 }
0254 
0255 void TaskView::startCurrentTimer()
0256 {
0257     startTimerForNow(m_tasksWidget->currentItem());
0258 }
0259 
0260 void TaskView::startTimerFor(Task *task, const QDateTime &startTime)
0261 {
0262     qCDebug(KTT_LOG) << "Entering function";
0263     if (task != nullptr && !task->isRunning()) {
0264         if (!task->isComplete()) {
0265             if (KTimeTrackerSettings::uniTasking()) {
0266                 stopAllTimers();
0267             }
0268             m_idleTimeDetector->startIdleDetection();
0269 
0270             task->setRunning(true, startTime);
0271         }
0272     }
0273 
0274     Q_EMIT updateButtons();
0275     Q_EMIT tasksChanged(storage()->tasksModel()->getActiveTasks());
0276 
0277     if (!storage()->tasksModel()->getActiveTasks().isEmpty()) {
0278         Q_EMIT timersActive();
0279     }
0280 }
0281 
0282 void TaskView::startTimerForNow(Task *task)
0283 {
0284     startTimerFor(task, QDateTime::currentDateTime());
0285 }
0286 
0287 void TaskView::stopAllTimers(const QDateTime &when)
0288 {
0289     qCDebug(KTT_LOG) << "Entering function";
0290     QProgressDialog dialog(i18nc("@info:progress", "Stopping timers..."),
0291                            i18n("Cancel"),
0292                            0,
0293                            storage()->tasksModel()->getActiveTasks().size(),
0294                            m_tasksWidget);
0295     if (storage()->tasksModel()->getActiveTasks().size() > 1) {
0296         dialog.show();
0297     }
0298 
0299     for (Task *task : storage()->tasksModel()->getActiveTasks()) {
0300         QApplication::processEvents();
0301 
0302         task->setRunning(false, when);
0303 
0304         dialog.setValue(dialog.value() + 1);
0305     }
0306 
0307     m_idleTimeDetector->stopIdleDetection();
0308     Q_EMIT updateButtons();
0309     Q_EMIT timersInactive();
0310     Q_EMIT tasksChanged(storage()->tasksModel()->getActiveTasks());
0311 }
0312 
0313 void TaskView::toggleFocusTracking()
0314 {
0315     m_focusTrackingActive = !m_focusTrackingActive;
0316 
0317     if (m_focusTrackingActive) {
0318         // FIXME: should get the currently active window and start tracking it?
0319     } else {
0320         stopTimerFor(m_lastTaskWithFocus);
0321     }
0322 
0323     Q_EMIT updateButtons();
0324 }
0325 
0326 void TaskView::stopTimerFor(Task *task)
0327 {
0328     qCDebug(KTT_LOG) << "Entering function";
0329     if (task != nullptr && task->isRunning()) {
0330         task->setRunning(false);
0331 
0332         if (storage()->tasksModel()->getActiveTasks().isEmpty()) {
0333             m_idleTimeDetector->stopIdleDetection();
0334             Q_EMIT timersInactive();
0335         }
0336         Q_EMIT updateButtons();
0337     }
0338     Q_EMIT tasksChanged(storage()->tasksModel()->getActiveTasks());
0339 }
0340 
0341 void TaskView::stopCurrentTimer()
0342 {
0343     stopTimerFor(m_tasksWidget->currentItem());
0344     if (m_focusTrackingActive && m_lastTaskWithFocus == m_tasksWidget->currentItem()) {
0345         toggleFocusTracking();
0346     }
0347 }
0348 
0349 void TaskView::minuteUpdate()
0350 {
0351     storage()->tasksModel()->addTimeToActiveTasks(1);
0352     Q_EMIT minutesUpdated(storage()->tasksModel()->getActiveTasks());
0353 }
0354 
0355 void TaskView::newTask(const QString &caption, Task *parent)
0356 {
0357     QPointer<TaskPropertiesDialog> dialog =
0358         new TaskPropertiesDialog(m_tasksWidget->parentWidget(), caption, QString(), QString(), DesktopList());
0359 
0360     if (dialog->exec() == QDialog::Accepted) {
0361         QString taskName = i18n("Unnamed Task");
0362         if (!dialog->name().isEmpty()) {
0363             taskName = dialog->name();
0364         }
0365         QString taskDescription = dialog->description();
0366 
0367         auto desktopList = dialog->desktops();
0368 
0369         // If all available desktops are checked, disable auto tracking,
0370         // since it makes no sense to track for every desktop.
0371         if (desktopList.size() == m_desktopTracker->desktopCount()) {
0372             desktopList.clear();
0373         }
0374 
0375         int64_t total = 0;
0376         int64_t session = 0;
0377         auto *task = addTask(taskName, taskDescription, total, session, desktopList, parent);
0378         if (!task) {
0379             KMessageBox::error(nullptr,
0380                                i18n("Error storing new task. Your changes were not saved. "
0381                                     "Make sure you can edit your iCalendar file. Also quit "
0382                                     "all applications using this file and remove any lock "
0383                                     "file related to its name from "
0384                                     "~/.kde/share/apps/kabc/lock/"));
0385         }
0386     }
0387     delete dialog;
0388 
0389     Q_EMIT updateButtons();
0390 }
0391 
0392 Task *TaskView::addTask(const QString &taskname,
0393                         const QString &taskdescription,
0394                         int64_t total,
0395                         int64_t session,
0396                         const DesktopList &desktops,
0397                         Task *parent)
0398 {
0399     qCDebug(KTT_LOG) << "Entering function; taskname =" << taskname;
0400     m_tasksWidget->setSortingEnabled(false);
0401 
0402     Task *task = new Task(taskname, taskdescription, total, session, desktops, storage()->projectModel(), parent);
0403     if (task->uid().isNull()) {
0404         qFatal("failed to generate UID");
0405     }
0406 
0407     m_desktopTracker->registerForDesktops(task, desktops);
0408     m_tasksWidget->setCurrentIndex(m_filterProxyModel->mapFromSource(storage()->tasksModel()->index(task, 0)));
0409     task->invalidateCompletedState();
0410 
0411     m_tasksWidget->setSortingEnabled(true);
0412     return task;
0413 }
0414 
0415 void TaskView::newSubTask()
0416 {
0417     Task *task = m_tasksWidget->currentItem();
0418     if (!task) {
0419         return;
0420     }
0421 
0422     newTask(i18nc("@title:window", "New Sub Task"), task);
0423 
0424     m_tasksWidget->setExpanded(m_filterProxyModel->mapFromSource(storage()->tasksModel()->index(task, 0)), true);
0425 
0426     storage()->projectModel()->refresh();
0427     tasksWidget()->refresh();
0428 }
0429 
0430 void TaskView::editTask()
0431 {
0432     qCDebug(KTT_LOG) << "Entering editTask";
0433     Task *task = m_tasksWidget->currentItem();
0434     if (!task) {
0435         return;
0436     }
0437 
0438     auto oldDeskTopList = task->desktops();
0439     QPointer<TaskPropertiesDialog> dialog = new TaskPropertiesDialog(m_tasksWidget->parentWidget(),
0440                                                                      i18nc("@title:window", "Edit Task"),
0441                                                                      task->name(),
0442                                                                      task->description(),
0443                                                                      oldDeskTopList);
0444     if (dialog->exec() == QDialog::Accepted) {
0445         QString name = i18n("Unnamed Task");
0446         if (!dialog->name().isEmpty()) {
0447             name = dialog->name();
0448         }
0449 
0450         // setName only does something if the new name is different
0451         task->setName(name);
0452         task->setDescription(dialog->description());
0453         auto desktopList = dialog->desktops();
0454         // If all available desktops are checked, disable auto tracking,
0455         // since it makes no sense to track for every desktop.
0456         if (desktopList.size() == m_desktopTracker->desktopCount()) {
0457             desktopList.clear();
0458         }
0459         // only do something for autotracking if the new setting is different
0460         if (oldDeskTopList != desktopList) {
0461             task->setDesktopList(desktopList);
0462             m_desktopTracker->registerForDesktops(task, desktopList);
0463         }
0464         Q_EMIT updateButtons();
0465     }
0466 
0467     delete dialog;
0468 }
0469 
0470 void TaskView::setPerCentComplete(int completion)
0471 {
0472     Task *task = m_tasksWidget->currentItem();
0473     if (!task) {
0474         KMessageBox::information(nullptr, i18n("No task selected."));
0475         return;
0476     }
0477 
0478     if (completion < 0) {
0479         completion = 0;
0480     }
0481     if (completion < 100) {
0482         task->setPercentComplete(completion);
0483         task->invalidateCompletedState();
0484         Q_EMIT updateButtons();
0485     }
0486 }
0487 
0488 void TaskView::deleteTaskBatch(Task *task)
0489 {
0490     QString uid = task->uid();
0491     task->remove();
0492     deleteEntry(uid); // forget if the item was expanded or collapsed
0493 
0494     task->delete_recursive();
0495 
0496     // Stop idle detection if no more counters are running
0497     if (storage()->tasksModel()->getActiveTasks().isEmpty()) {
0498         m_idleTimeDetector->stopIdleDetection();
0499         Q_EMIT timersInactive();
0500     }
0501 
0502     Q_EMIT tasksChanged(storage()->tasksModel()->getActiveTasks());
0503 }
0504 
0505 void TaskView::deleteTask(Task *task)
0506 /* Attention when popping up a window asking for confirmation.
0507 If you have "Track active applications" on, this window will create a new task and
0508 make this task running and selected. */
0509 {
0510     if (!task) {
0511         task = m_tasksWidget->currentItem();
0512     }
0513 
0514     if (!m_tasksWidget->currentItem()) {
0515         KMessageBox::information(nullptr, i18n("No task selected."));
0516     } else {
0517         int response = KMessageBox::Continue;
0518         if (KTimeTrackerSettings::promptDelete()) {
0519             response = KMessageBox::warningContinueCancel(nullptr,
0520                                                           i18n("Are you sure you want to delete the selected task and "
0521                                                                "its entire history?\n"
0522                                                                "Note: All subtasks and their history will also be "
0523                                                                "deleted."),
0524                                                           i18nc("@title:window", "Deleting Task"),
0525                                                           KStandardGuiItem::del());
0526         }
0527 
0528         if (response == KMessageBox::Continue) {
0529             deleteTaskBatch(task);
0530         }
0531     }
0532 }
0533 
0534 void TaskView::markTaskAsComplete()
0535 {
0536     if (!m_tasksWidget->currentItem()) {
0537         KMessageBox::information(nullptr, i18n("No task selected."));
0538         return;
0539     }
0540 
0541     m_tasksWidget->currentItem()->setPercentComplete(100);
0542     m_tasksWidget->currentItem()->invalidateCompletedState();
0543     Q_EMIT updateButtons();
0544 }
0545 
0546 void TaskView::subtractTime(int64_t minutes)
0547 {
0548     storage()->tasksModel()->addTimeToActiveTasks(-minutes);
0549 }
0550 
0551 void TaskView::markTaskAsIncomplete()
0552 {
0553     setPerCentComplete(50); // if it has been reopened, assume half-done
0554 }
0555 
0556 void TaskView::slotColumnToggled(int column)
0557 {
0558     switch (column) {
0559     case 1:
0560         KTimeTrackerSettings::setDisplaySessionTime(!m_tasksWidget->isColumnHidden(1));
0561         break;
0562     case 2:
0563         KTimeTrackerSettings::setDisplayTime(!m_tasksWidget->isColumnHidden(2));
0564         break;
0565     case 3:
0566         KTimeTrackerSettings::setDisplayTotalSessionTime(!m_tasksWidget->isColumnHidden(3));
0567         break;
0568     case 4:
0569         KTimeTrackerSettings::setDisplayTotalTime(!m_tasksWidget->isColumnHidden(4));
0570         break;
0571     case 5:
0572         KTimeTrackerSettings::setDisplayPriority(!m_tasksWidget->isColumnHidden(5));
0573         break;
0574     case 6:
0575         KTimeTrackerSettings::setDisplayPercentComplete(!m_tasksWidget->isColumnHidden(6));
0576         break;
0577     }
0578     KTimeTrackerSettings::self()->save();
0579 }
0580 
0581 bool TaskView::isFocusTrackingActive() const
0582 {
0583     return m_focusTrackingActive;
0584 }
0585 
0586 void TaskView::reconfigureModel()
0587 {
0588     /* idleness */
0589     m_idleTimeDetector->setMaxIdle(KTimeTrackerSettings::period());
0590     m_idleTimeDetector->toggleOverAllIdleDetection(KTimeTrackerSettings::enabled());
0591 
0592     /* auto save */
0593     if (KTimeTrackerSettings::autoSave()) {
0594         m_autoSaveTimer->start(KTimeTrackerSettings::autoSavePeriod() * 1000 * secsPerMinute);
0595     } else if (m_autoSaveTimer->isActive()) {
0596         m_autoSaveTimer->stop();
0597     }
0598 
0599     storage()->projectModel()->refresh();
0600 }
0601 
0602 //----------------------------------------------------------------------------
0603 
0604 void TaskView::onTaskDoubleClicked(Task *task)
0605 {
0606     if (task->isRunning()) {
0607         // if task is running, stop it
0608         stopCurrentTimer();
0609     } else if (!task->isComplete()) {
0610         // if task is not running, start it
0611         stopAllTimers();
0612         startCurrentTimer();
0613     }
0614 }
0615 
0616 void TaskView::editTaskTime(const QString &taskUid, int64_t minutes)
0617 {
0618     // update session time if the time was changed
0619     auto *task = m_storage->tasksModel()->taskByUID(taskUid);
0620     if (task) {
0621         task->changeTime(minutes, m_storage->eventsModel());
0622     }
0623 }
0624 
0625 void TaskView::taskAboutToBeRemoved(const QModelIndex &parent, int first, int last)
0626 {
0627     if (first != last) {
0628         qFatal(
0629             "taskAboutToBeRemoved: unexpected removal of multiple items at "
0630             "once");
0631     }
0632 
0633     TasksModelItem *item = nullptr;
0634     if (parent.isValid()) {
0635         // Nested task
0636         auto *parentItem = storage()->tasksModel()->item(parent);
0637         if (!parentItem) {
0638             qFatal("taskAboutToBeRemoved: parentItem is nullptr");
0639         }
0640 
0641         item = parentItem->child(first);
0642     } else {
0643         // Top-level task
0644         item = storage()->tasksModel()->topLevelItem(first);
0645     }
0646 
0647     if (!item) {
0648         qFatal("taskAboutToBeRemoved: item is nullptr");
0649     }
0650 
0651     // We use static_cast here instead of dynamic_cast because this
0652     // taskAboutToBeRemoved() slot is called from TasksModelItem's destructor
0653     // when the Task object is already destructed, thus dynamic_cast would
0654     // return nullptr.
0655     auto *task = dynamic_cast<Task *>(item);
0656     if (!task) {
0657         qFatal("taskAboutToBeRemoved: task is nullptr");
0658     }
0659 
0660     // Handle task deletion
0661     m_desktopTracker->registerForDesktops(task, {});
0662 }
0663 
0664 void TaskView::taskRemoved(const QModelIndex & /*parent*/, int /*first*/, int /*last*/)
0665 {
0666     Q_EMIT tasksChanged(storage()->tasksModel()->getActiveTasks());
0667 }
0668 
0669 #include "moc_taskview.cpp"