diff --git a/src/taskview.cpp b/src/taskview.cpp index 6934243..3b9578f 100644 --- a/src/taskview.cpp +++ b/src/taskview.cpp @@ -1,665 +1,653 @@ /* * Copyright (C) 2003 by Scott Monachello * Copyright (C) 2019 Alexander Potashev * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #include "taskview.h" #include #include #include #include #include #include #include "dialogs/exportdialog.h" #include "desktoptracker.h" #include "dialogs/edittimedialog.h" #include "dialogs/taskpropertiesdialog.h" #include "export/export.h" #include "focusdetector.h" #include "dialogs/historydialog.h" #include "idletimedetector.h" #include "import/plannerparser.h" #include "ktimetracker.h" #include "ktimetrackerutility.h" #include "ktt_debug.h" #include "model/eventsmodel.h" #include "model/projectmodel.h" #include "model/task.h" #include "model/tasksmodel.h" #include "treeviewheadercontextmenu.h" #include "widgets/taskswidget.h" void deleteEntry(const QString& key) { KConfigGroup config = KSharedConfig::openConfig()->group(QString()); config.deleteEntry(key); config.sync(); } TaskView::TaskView(QWidget *parent) : QObject(parent) , m_filterProxyModel(new QSortFilterProxyModel(this)) , m_storage(new TimeTrackerStorage()) , m_focusTrackingActive(false) , m_lastTaskWithFocus(nullptr) , m_focusDetector(new FocusDetector()) , m_tasksWidget(nullptr) { m_filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterProxyModel->setRecursiveFilteringEnabled(true); m_filterProxyModel->setSortRole(Task::SortRole); connect(m_focusDetector, &FocusDetector::newFocus, this, &TaskView::newFocusWindowDetected); // set up the minuteTimer m_minuteTimer = new QTimer(this); connect(m_minuteTimer, &QTimer::timeout, this, &TaskView::minuteUpdate); m_minuteTimer->start(1000 * secsPerMinute); // Set up the idle detection. m_idleTimeDetector = new IdleTimeDetector(KTimeTrackerSettings::period()); connect(m_idleTimeDetector, &IdleTimeDetector::subtractTime, this, &TaskView::subtractTime); connect(m_idleTimeDetector, &IdleTimeDetector::stopAllTimers, this, &TaskView::stopAllTimers); if (!IdleTimeDetector::isIdleDetectionPossible()) { KTimeTrackerSettings::setEnabled(false); } // Setup auto save timer m_autoSaveTimer = new QTimer(this); connect(m_autoSaveTimer, &QTimer::timeout, this, &TaskView::save); // Connect desktop tracker events to task starting/stopping m_desktopTracker = new DesktopTracker(); connect(m_desktopTracker, &DesktopTracker::reachedActiveDesktop, this, &TaskView::startTimerForNow); connect(m_desktopTracker, &DesktopTracker::leftActiveDesktop, this, &TaskView::stopTimerFor); } void TaskView::newFocusWindowDetected(const QString &taskName) { QString newTaskName = taskName; newTaskName.remove('\n'); if (!m_focusTrackingActive) { return; } bool found = false; // has taskName been found in our tasks stopTimerFor(m_lastTaskWithFocus); for (Task *task : storage()->tasksModel()->getAllTasks()) { if (task->name() == newTaskName) { found = true; startTimerForNow(task); m_lastTaskWithFocus = task; } } if (!found) { if (!addTask(newTaskName)) { KMessageBox::error( nullptr, i18n("Error storing new task. Your changes were not saved. " "Make sure you can edit your iCalendar file. " "Also quit all applications using this file and remove " "any lock file related to its name from ~/.kde/share/apps/kabc/lock/ ")); } for (Task *task : storage()->tasksModel()->getAllTasks()) { if (task->name() == newTaskName) { startTimerForNow(task); m_lastTaskWithFocus = task; } } } emit updateButtons(); } TimeTrackerStorage *TaskView::storage() { return m_storage; } TaskView::~TaskView() { delete m_storage; KTimeTrackerSettings::self()->save(); } void TaskView::load(const QUrl &url) { if (m_tasksWidget) { qFatal("TaskView::load must be called only once"); } // if the program is used as an embedded plugin for konqueror, there may be a need // to load from a file without touching the preferences. QString err = m_storage->load(this, url); if (!err.isEmpty()) { KMessageBox::error(m_tasksWidget, err); qCDebug(KTT_LOG) << "Leaving TaskView::load"; return; } m_tasksWidget = new TasksWidget(dynamic_cast(parent()), m_filterProxyModel, nullptr); connect(m_tasksWidget, &TasksWidget::updateButtons, this, &TaskView::updateButtons); connect(m_tasksWidget, &TasksWidget::contextMenuRequested, this, &TaskView::contextMenuRequested); connect(m_tasksWidget, &TasksWidget::taskDoubleClicked, this, &TaskView::onTaskDoubleClicked); m_tasksWidget->setRootIsDecorated(true); reconfigureModel(); m_tasksWidget->reconfigure(); // Connect to the new model created by TimeTrackerStorage::load() auto *tasksModel = m_storage->tasksModel(); m_filterProxyModel->setSourceModel(tasksModel); m_tasksWidget->setSourceModel(tasksModel); for (int i = 0; i <= tasksModel->columnCount(QModelIndex()); ++i) { m_tasksWidget->resizeColumnToContents(i); } // Table header context menu auto *headerContextMenu = new TreeViewHeaderContextMenu(this, m_tasksWidget, QVector{0}); connect(headerContextMenu, &TreeViewHeaderContextMenu::columnToggled, this, &TaskView::slotColumnToggled); connect(tasksModel, &TasksModel::taskCompleted, this, &TaskView::stopTimerFor); connect(tasksModel, &TasksModel::taskDropped, this, &TaskView::reFreshTimes); connect(tasksModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &TaskView::taskAboutToBeRemoved); connect(tasksModel, &QAbstractItemModel::rowsRemoved, this, &TaskView::taskRemoved); connect(storage()->eventsModel(), &EventsModel::timesChanged, this, &TaskView::reFreshTimes); // Register tasks with desktop tracker for (Task *task : storage()->tasksModel()->getAllTasks()) { m_desktopTracker->registerForDesktops(task, task->desktops()); } // Start all tasks that have an event without endtime for (Task *task : storage()->tasksModel()->getAllTasks()) { if (!m_storage->allEventsHaveEndTime(task)) { task->resumeRunning(); - m_activeTasks.append(task); } } emit updateButtons(); emit tasksChanged(storage()->tasksModel()->getActiveTasks()); if (storage()->tasksModel()->getActiveTasks().isEmpty()) { emit timersInactive(); } else { emit timersActive(); } if (tasksModel->topLevelItemCount() > 0) { m_tasksWidget->restoreItemState(); m_tasksWidget->setCurrentIndex(m_filterProxyModel->mapFromSource( tasksModel->index(tasksModel->topLevelItem(0), 0))); if (!m_desktopTracker->startTracking().isEmpty()) { KMessageBox::error(nullptr, i18n("Your virtual desktop number is too high, desktop tracking will not work.")); } } storage()->projectModel()->refresh(); tasksWidget()->refresh(); for (int i = 0; i <= tasksModel->columnCount(QModelIndex()); ++i) { m_tasksWidget->resizeColumnToContents(i); } } void TaskView::closeStorage() { m_storage->closeStorage(); } QString TaskView::reFreshTimes() { storage()->projectModel()->refreshTimes(); tasksWidget()->refresh(); return QString(); } void TaskView::importPlanner(const QString& fileName) { storage()->projectModel()->importPlanner(fileName, m_tasksWidget->currentItem()); tasksWidget()->refresh(); } void TaskView::save() { qCDebug(KTT_LOG) << "Entering TaskView::save()"; QString err = m_storage->save(); if (!err.isNull()) { KMessageBox::error(m_tasksWidget, err); } } void TaskView::startCurrentTimer() { startTimerForNow(m_tasksWidget->currentItem()); } void TaskView::startTimerFor(Task *task, const QDateTime &startTime) { qCDebug(KTT_LOG) << "Entering function"; if (task != nullptr && !task->isRunning()) { if (!task->isComplete()) { if (KTimeTrackerSettings::uniTasking()) { stopAllTimers(); } m_idleTimeDetector->startIdleDetection(); task->setRunning(true, startTime); - - m_activeTasks.append(task); } } emit updateButtons(); emit tasksChanged(storage()->tasksModel()->getActiveTasks()); if (!storage()->tasksModel()->getActiveTasks().isEmpty()) { emit timersActive(); } } void TaskView::startTimerForNow(Task *task) { startTimerFor(task, QDateTime::currentDateTime()); } -void TaskView::clearActiveTasks() -{ - m_activeTasks.clear(); -} - void TaskView::stopAllTimers(const QDateTime& when) { qCDebug(KTT_LOG) << "Entering function"; QProgressDialog dialog( i18nc("@info:progress", "Stopping timers..."), i18n("Cancel"), 0, storage()->tasksModel()->getActiveTasks().size(), m_tasksWidget); if (storage()->tasksModel()->getActiveTasks().size() > 1) { dialog.show(); } for (Task *task : storage()->tasksModel()->getActiveTasks()) { QApplication::processEvents(); task->setRunning(false, when); dialog.setValue(dialog.value() + 1); } m_idleTimeDetector->stopIdleDetection(); - m_activeTasks.clear(); emit updateButtons(); emit timersInactive(); emit tasksChanged(storage()->tasksModel()->getActiveTasks()); } void TaskView::toggleFocusTracking() { m_focusTrackingActive = !m_focusTrackingActive; if (m_focusTrackingActive) { // FIXME: should get the currently active window and start tracking it? } else { stopTimerFor(m_lastTaskWithFocus); } emit updateButtons(); } void TaskView::stopTimerFor(Task* task) { qCDebug(KTT_LOG) << "Entering function"; if (task != nullptr && task->isRunning()) { - m_activeTasks.removeAll(task); - task->setRunning(false); if (storage()->tasksModel()->getActiveTasks().isEmpty()) { m_idleTimeDetector->stopIdleDetection(); emit timersInactive(); } emit updateButtons(); } emit tasksChanged(storage()->tasksModel()->getActiveTasks()); } void TaskView::stopCurrentTimer() { stopTimerFor(m_tasksWidget->currentItem()); if (m_focusTrackingActive && m_lastTaskWithFocus == m_tasksWidget->currentItem()) { toggleFocusTracking(); } } void TaskView::minuteUpdate() { storage()->tasksModel()->addTimeToActiveTasks(1); } void TaskView::newTask(const QString &caption, Task *parent) { QPointer dialog = new TaskPropertiesDialog( m_tasksWidget->parentWidget(), caption, QString(), QString(), DesktopList()); if (dialog->exec() == QDialog::Accepted) { QString taskName = i18n("Unnamed Task"); if (!dialog->name().isEmpty()) { taskName = dialog->name(); } QString taskDescription = dialog->description(); auto desktopList = dialog->desktops(); // If all available desktops are checked, disable auto tracking, // since it makes no sense to track for every desktop. if (desktopList.size() == m_desktopTracker->desktopCount()) { desktopList.clear(); } int64_t total = 0; int64_t session = 0; auto *task = addTask(taskName, taskDescription, total, session, desktopList, parent); if (!task) { KMessageBox::error(nullptr, i18n( "Error storing new task. Your changes were not saved. " "Make sure you can edit your iCalendar file. Also quit " "all applications using this file and remove any lock " "file related to its name from ~/.kde/share/apps/kabc/lock/")); } } delete dialog; emit updateButtons(); } Task *TaskView::addTask( const QString& taskname, const QString& taskdescription, int64_t total, int64_t session, const DesktopList& desktops, Task* parent) { qCDebug(KTT_LOG) << "Entering function; taskname =" << taskname; m_tasksWidget->setSortingEnabled(false); Task *task = new Task( taskname, taskdescription, total, session, desktops, storage()->projectModel(), parent); if (task->uid().isNull()) { qFatal("failed to generate UID"); } m_desktopTracker->registerForDesktops(task, desktops); m_tasksWidget->setCurrentIndex(m_filterProxyModel->mapFromSource(storage()->tasksModel()->index(task, 0))); task->invalidateCompletedState(); m_tasksWidget->setSortingEnabled(true); return task; } void TaskView::newSubTask() { Task* task = m_tasksWidget->currentItem(); if (!task) { return; } newTask(i18nc("@title:window", "New Sub Task"), task); m_tasksWidget->setExpanded(m_filterProxyModel->mapFromSource(storage()->tasksModel()->index(task, 0)), true); storage()->projectModel()->refresh(); tasksWidget()->refresh(); } void TaskView::editTask() { qCDebug(KTT_LOG) <<"Entering editTask"; Task* task = m_tasksWidget->currentItem(); if (!task) { return; } auto oldDeskTopList = task->desktops(); QPointer dialog = new TaskPropertiesDialog( m_tasksWidget->parentWidget(), i18nc("@title:window", "Edit Task"), task->name(), task->description(), oldDeskTopList); if (dialog->exec() == QDialog::Accepted) { QString name = i18n("Unnamed Task"); if (!dialog->name().isEmpty()) { name = dialog->name(); } // setName only does something if the new name is different task->setName(name); task->setDescription(dialog->description()); auto desktopList = dialog->desktops(); // If all available desktops are checked, disable auto tracking, // since it makes no sense to track for every desktop. if (desktopList.size() == m_desktopTracker->desktopCount()) { desktopList.clear(); } // only do something for autotracking if the new setting is different if (oldDeskTopList != desktopList) { task->setDesktopList(desktopList); m_desktopTracker->registerForDesktops(task, desktopList); } emit updateButtons(); } delete dialog; } void TaskView::setPerCentComplete(int completion) { Task* task = m_tasksWidget->currentItem(); if (!task) { KMessageBox::information(nullptr, i18n("No task selected.")); return; } if (completion < 0) { completion = 0; } if (completion < 100) { task->setPercentComplete(completion); task->invalidateCompletedState(); emit updateButtons(); } } void TaskView::deleteTaskBatch(Task* task) { QString uid = task->uid(); task->remove(); deleteEntry(uid); // forget if the item was expanded or collapsed task->delete_recursive(); // Stop idle detection if no more counters are running if (storage()->tasksModel()->getActiveTasks().isEmpty()) { m_idleTimeDetector->stopIdleDetection(); emit timersInactive(); } emit tasksChanged(storage()->tasksModel()->getActiveTasks()); } void TaskView::deleteTask(Task* task) /* Attention when popping up a window asking for confirmation. If you have "Track active applications" on, this window will create a new task and make this task running and selected. */ { if (!task) { task = m_tasksWidget->currentItem(); } if (!m_tasksWidget->currentItem()) { KMessageBox::information(nullptr, i18n("No task selected.")); } else { int response = KMessageBox::Continue; if (KTimeTrackerSettings::promptDelete()) { response = KMessageBox::warningContinueCancel(nullptr, i18n("Are you sure you want to delete the selected task and its entire history?\n" "Note: All subtasks and their history will also be deleted."), i18nc("@title:window", "Deleting Task"), KStandardGuiItem::del()); } if (response == KMessageBox::Continue) { deleteTaskBatch(task); } } } void TaskView::markTaskAsComplete() { if (!m_tasksWidget->currentItem()) { KMessageBox::information(nullptr, i18n("No task selected.")); return; } m_tasksWidget->currentItem()->setPercentComplete(100); m_tasksWidget->currentItem()->invalidateCompletedState(); emit updateButtons(); } void TaskView::subtractTime(int64_t minutes) { storage()->tasksModel()->addTimeToActiveTasks(-minutes); } void TaskView::markTaskAsIncomplete() { setPerCentComplete(50); // if it has been reopened, assume half-done } void TaskView::slotColumnToggled(int column) { switch (column) { case 1: KTimeTrackerSettings::setDisplaySessionTime(!m_tasksWidget->isColumnHidden(1)); break; case 2: KTimeTrackerSettings::setDisplayTime(!m_tasksWidget->isColumnHidden(2)); break; case 3: KTimeTrackerSettings::setDisplayTotalSessionTime(!m_tasksWidget->isColumnHidden(3)); break; case 4: KTimeTrackerSettings::setDisplayTotalTime(!m_tasksWidget->isColumnHidden(4)); break; case 5: KTimeTrackerSettings::setDisplayPriority(!m_tasksWidget->isColumnHidden(5)); break; case 6: KTimeTrackerSettings::setDisplayPercentComplete(!m_tasksWidget->isColumnHidden(6)); break; } KTimeTrackerSettings::self()->save(); } bool TaskView::isFocusTrackingActive() const { return m_focusTrackingActive; } void TaskView::reconfigureModel() { /* idleness */ m_idleTimeDetector->setMaxIdle(KTimeTrackerSettings::period()); m_idleTimeDetector->toggleOverAllIdleDetection(KTimeTrackerSettings::enabled()); /* auto save */ if (KTimeTrackerSettings::autoSave()) { m_autoSaveTimer->start(KTimeTrackerSettings::autoSavePeriod() * 1000 * secsPerMinute); } else if (m_autoSaveTimer->isActive()) { m_autoSaveTimer->stop(); } storage()->projectModel()->refresh(); } //---------------------------------------------------------------------------- void TaskView::onTaskDoubleClicked(Task *task) { if (task->isRunning()) { // if task is running, stop it stopCurrentTimer(); } else if (!task->isComplete()) { // if task is not running, start it stopAllTimers(); startCurrentTimer(); } } void TaskView::editTaskTime(const QString& taskUid, int64_t minutes) { // update session time if the time was changed auto* task = m_storage->tasksModel()->taskByUID(taskUid); if (task) { task->changeTime(minutes, m_storage->eventsModel()); } } void TaskView::taskAboutToBeRemoved(const QModelIndex &parent, int first, int last) { if (first != last) { qFatal("taskAboutToBeRemoved: unexpected removal of multiple items at once"); } TasksModelItem *item = nullptr; if (parent.isValid()) { // Nested task auto *parentItem = storage()->tasksModel()->item(parent); if (!parentItem) { qFatal("taskAboutToBeRemoved: parentItem is nullptr"); } item = parentItem->child(first); } else { // Top-level task item = storage()->tasksModel()->topLevelItem(first); } if (!item) { qFatal("taskAboutToBeRemoved: item is nullptr"); } // We use static_cast here instead of dynamic_cast because this // taskAboutToBeRemoved() slot is called from TasksModelItem's destructor // when the Task object is already destructed, thus dynamic_cast would // return nullptr. auto *task = dynamic_cast(item); if (!task) { qFatal("taskAboutToBeRemoved: task is nullptr"); } // Handle task deletion m_desktopTracker->registerForDesktops(task, {}); - m_activeTasks.removeAll(task); } void TaskView::taskRemoved(const QModelIndex &/*parent*/, int /*first*/, int /*last*/) { emit tasksChanged(storage()->tasksModel()->getActiveTasks()); } diff --git a/src/taskview.h b/src/taskview.h index 545ac98..94e99be 100644 --- a/src/taskview.h +++ b/src/taskview.h @@ -1,213 +1,207 @@ /* * Copyright (C) 2003 by Scott Monachello * Copyright (C) 2019 Alexander Potashev * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ #ifndef KTIMETRACKER_TASK_VIEW #define KTIMETRACKER_TASK_VIEW #include #include "desktoplist.h" #include "timetrackerstorage.h" #include "reportcriteria.h" QT_BEGIN_NAMESPACE class QTimer; class QSortFilterProxyModel; class QMenu; QT_END_NAMESPACE class DesktopTracker; class IdleTimeDetector; class Task; class TimeTrackerStorage; class FocusDetector; class TasksModel; class TasksWidget; /** * Container and interface for the tasks. */ class TaskView : public QObject { Q_OBJECT public: explicit TaskView(QWidget *parent = nullptr); ~TaskView() override; /** * Load the view from storage. * * Must be called only once. If you need to open another file, * please create a new TaskView object. */ void load(const QUrl& url); /** Close the storage and release lock. */ void closeStorage(); /** Add a task to view and storage. */ Task *addTask( const QString& taskname, const QString& taskdescription = QString(), int64_t total = 0, int64_t session = 0, const DesktopList& desktops = QVector(0,0), Task* parent = nullptr); /** Returns whether the focus tracking is currently active. */ bool isFocusTrackingActive() const; TasksWidget *tasksWidget() { return m_tasksWidget; } public Q_SLOTS: /** Save to persistent storage. */ void save(); /** Start the timer on the current item (task) in view. */ void startCurrentTimer(); /** Stop the timer for the current item in the view. */ void stopCurrentTimer(); /** Stop all running timers. * @param when When the timer stopped - this makes sense if the idletime- * detector detects the user stopped working 5 minutes ago. */ void stopAllTimers(const QDateTime &when = QDateTime::currentDateTime()); /** Toggles the automatic tracking of focused windows */ void toggleFocusTracking(); /** Display edit task dialog and create a new task with results. * @param caption Window title of the edit task dialog */ void newTask(const QString &caption, Task *parent); /** used to import tasks from imendio planner */ void importPlanner(const QString &fileName); /** Calls newTask dialog with caption "New Sub Task". */ void newSubTask(); /** Calls editTask dialog for the current task. */ void editTask(); /** * Returns a pointer to storage object. * * This is poor object oriented design--the task view should * expose wrappers around the storage methods we want to access instead of * giving clients full access to objects that we own. * * Hopefully, this will be redesigned as part of the Qt4 migration. */ TimeTrackerStorage* storage(); /** * Deletes the given or the current task (and children) from the view. * It does this in batch mode, no user dialog. * @param task Task to be deleted. If empty, the current task is deleted. * if non-existent, an error message is displayed. */ void deleteTaskBatch(Task *task); /** * Deletes the given or the current task (and children) from the view. * Depending on configuration, there may be a user dialog. * @param task Task to be deleted. If empty, the current task is deleted. * if non-existent, an error message is displayed. */ void deleteTask(Task* task = nullptr); /** Sets % completed for the current task. * @param completion The percentage complete to mark the task as. */ void setPerCentComplete(int completion); void markTaskAsComplete(); void markTaskAsIncomplete(); /** Subtracts time from all active tasks, and does not log event. */ void subtractTime(int64_t minutes); /** receiving signal that a task is being deleted */ void taskAboutToBeRemoved(const QModelIndex &parent, int first, int last); /** receiving signal that a task has been deleted */ void taskRemoved(const QModelIndex &parent, int first, int last); /** starts timer for task. * @param task task to start timer of * @param startTime if taskview has been modified by another program, we have to set the starting time to not-now. */ void startTimerFor(Task *task, const QDateTime& startTime); void startTimerForNow(Task *task); void stopTimerFor(Task *task); - /** clears all active tasks. Needed e.g. if iCal file was modified by - another program and taskview is cleared without stopping tasks - IF YOU DO NOT KNOW WHAT YOU ARE DOING, CALL stopAllTimers INSTEAD */ - void clearActiveTasks(); - /** Reconfigures taskView depending on current configuration. */ void reconfigureModel(); /** Refresh the times of the tasks, e.g. when the history has been changed by the user */ QString reFreshTimes(); void onTaskDoubleClicked(Task *task); void editTaskTime(const QString& taskUid, int64_t minutes); Q_SIGNALS: void updateButtons(); void timersActive(); void timersInactive(); /** Used to update text in tray icon */ void tasksChanged(const QList &activeTasks); void setStatusBarText(const QString &text); void contextMenuRequested(const QPoint&); private: // member variables QSortFilterProxyModel* m_filterProxyModel; IdleTimeDetector* m_idleTimeDetector; QTimer *m_minuteTimer; QTimer *m_autoSaveTimer; DesktopTracker* m_desktopTracker; TimeTrackerStorage *m_storage; bool m_focusTrackingActive; Task* m_lastTaskWithFocus; - QList m_activeTasks; FocusDetector *m_focusDetector; TasksWidget *m_tasksWidget; public Q_SLOTS: void minuteUpdate(); /** React on the focus having changed to Window QString **/ void newFocusWindowDetected(const QString&); void slotColumnToggled(int); }; #endif // KTIMETRACKER_TASK_VIEW diff --git a/src/timetrackerstorage.cpp b/src/timetrackerstorage.cpp index 06fb340..49de7e7 100644 --- a/src/timetrackerstorage.cpp +++ b/src/timetrackerstorage.cpp @@ -1,277 +1,276 @@ /* * Copyright (C) 2003, 2004 by Mark Bucciarelli * Copyright (C) 2019 Alexander Potashev * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA. * */ /** TimeTrackerStorage * This class cares for the storage of ktimetracker's data. * ktimetracker's data is * - tasks like "programming for customer foo" * - events like "from 2009-09-07, 8pm till 10pm programming for customer foo" * tasks are like the items on your todo list, events are dates when you worked on them. * ktimetracker's data is stored in a ResourceCalendar object hold by mCalendar. */ #include "timetrackerstorage.h" #include #include #include #include #include #include "ktt_debug.h" #include "model/eventsmodel.h" #include "model/projectmodel.h" #include "model/task.h" #include "model/tasksmodel.h" #include "taskview.h" #include "widgets/taskswidget.h" const QByteArray eventAppName = QByteArray("ktimetracker"); TimeTrackerStorage::TimeTrackerStorage() : m_model(nullptr) , m_taskView(nullptr) { } // Loads data from filename into view. QString TimeTrackerStorage::load(TaskView *view, const QUrl &url) { if (url.isEmpty()) { return QStringLiteral("TimeTrackerStorage::load() callled with an empty URL"); } // loading might create the file bool removedFromDirWatch = false; if (KDirWatch::self()->contains(m_url.toLocalFile())) { KDirWatch::self()->removeFile(m_url.toLocalFile()); removedFromDirWatch = true; } // If same file, don't reload if (url == m_url) { if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.toLocalFile()); } return QString(); } if (m_model) { closeStorage(); } m_model = new ProjectModel(); if (url.isLocalFile()) { connect(KDirWatch::self(), &KDirWatch::dirty, this, &TimeTrackerStorage::onFileModified); if (!KDirWatch::self()->contains(url.toLocalFile())) { KDirWatch::self()->addFile(url.toLocalFile()); } } // Create local file resource and add to resources m_url = url; FileCalendar m_calendar(m_url); m_taskView = view; // m_calendar->setTimeSpec( KSystemTimeZones::local() ); m_calendar.reload(); // Build task view from iCal data QString err; eventsModel()->load(m_calendar.rawEvents()); err = buildTaskView(m_calendar.rawTodos(), view); if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.toLocalFile()); } return err; } QUrl TimeTrackerStorage::fileUrl() { return m_url; } QString TimeTrackerStorage::buildTaskView(const KCalendarCore::Todo::List &todos, TaskView *view) // makes *view contain the tasks out of *rc. { // remember tasks that are running and their start times QVector runningTasks; QVector startTimes; for (Task *task : tasksModel()->getActiveTasks()) { runningTasks.append(task->uid()); startTimes.append(task->startTime()); } tasksModel()->clear(); QMultiHash map; for (const auto &todo : todos) { Task* task = new Task(todo, m_model); map.insert(todo->uid(), task); task->invalidateCompletedState(); } // 1.1. Load each task under its parent task. QString err; for (const auto &todo : todos) { Task* task = map.value(todo->uid()); // No relatedTo incident just means this is a top-level task. if (!todo->relatedTo().isEmpty()) { Task* newParent = map.value(todo->relatedTo()); // Complete the loading but return a message if (!newParent) { err = i18n("Error loading \"%1\": could not find parent (uid=%2)", task->name(), todo->relatedTo()); } else { task->move(newParent); } } } - view->clearActiveTasks(); // restart tasks that have been running with their start times for (Task *task : tasksModel()->getAllTasks()) { for (int n = 0; n < runningTasks.count(); ++n) { if (runningTasks[n] == task->uid()) { view->startTimerFor(task, startTimes[n]); } } } return err; } void TimeTrackerStorage::closeStorage() { if (m_model) { delete m_model; m_model = nullptr; } } EventsModel *TimeTrackerStorage::eventsModel() { if (!m_model) { qFatal("TimeTrackerStorage::eventsModel is nullptr"); } return m_model->eventsModel(); } TasksModel *TimeTrackerStorage::tasksModel() { if (!m_model) { qFatal("TimeTrackerStorage::tasksModel is nullptr"); } return m_model->tasksModel(); } ProjectModel *TimeTrackerStorage::projectModel() { if (!m_model) { qFatal("TimeTrackerStorage::projectModel is nullptr"); } return m_model; } bool TimeTrackerStorage::allEventsHaveEndTime(Task *task) { for (const auto *event : m_model->eventsModel()->eventsForTask(task)) { if (!event->hasEndDate()) { return false; } } return true; } QString TimeTrackerStorage::save() { bool removedFromDirWatch = false; if (KDirWatch::self()->contains(m_url.toLocalFile())) { KDirWatch::self()->removeFile(m_url.toLocalFile()); removedFromDirWatch = true; } if (!m_model) { qCWarning(KTT_LOG) << "TimeTrackerStorage::save: m_model is nullptr"; // No i18n() here because it's too technical and unlikely to happen return QStringLiteral("m_model is nullptr"); } const QString fileLockPath("ktimetrackerics.lock"); QLockFile fileLock(fileLockPath); if (!fileLock.lock()) { qCWarning(KTT_LOG) << "TimeTrackerStorage::save: m_fileLock->lock() failed"; return i18nc("%1=lock file path", "Could not write lock file \"%1\". Disk full?", fileLockPath); } QString errorMessage; std::unique_ptr calendar = m_model->asCalendar(m_url); if (!calendar->save()) { qCWarning(KTT_LOG) << "TimeTrackerStorage::save: calendar->save() failed"; errorMessage = i18nc("%1=destination file path/URL", "Failed to save iCalendar file as \"%1\".", m_url.toString()); } else { qCDebug(KTT_LOG) << "TimeTrackerStorage::save: wrote tasks to" << m_url; } fileLock.unlock(); if (removedFromDirWatch) { KDirWatch::self()->addFile(m_url.toLocalFile()); } return errorMessage; } //---------------------------------------------------------------------------- bool TimeTrackerStorage::bookTime(const Task* task, const QDateTime& startDateTime, int64_t durationInSeconds) { return eventsModel()->bookTime(task, startDateTime, durationInSeconds); } void TimeTrackerStorage::onFileModified() { if (!m_model) { qCWarning(KTT_LOG) << "TaskView::onFileModified(): model is null"; return; } // TODO resolve conflicts if KTimeTracker has unsaved changes in its data structures qCDebug(KTT_LOG) << "entering function"; FileCalendar m_calendar(m_url); // m_calendar->setTimeSpec( KSystemTimeZones::local() ); m_calendar.reload(); buildTaskView(m_calendar.rawTodos(), m_taskView); m_taskView->storage()->projectModel()->refresh(); m_taskView->tasksWidget()->refresh(); qCDebug(KTT_LOG) << "exiting onFileModified"; }