diff --git a/src/model/tasksmodel.cpp b/src/model/tasksmodel.cpp index 106fc00..1d39bf4 100644 --- a/src/model/tasksmodel.cpp +++ b/src/model/tasksmodel.cpp @@ -1,370 +1,377 @@ /* * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #include "tasksmodel.h" #include #include #include #include #include "task.h" TasksModel::TasksModel() : m_rootItem(new TasksModelItem(this, nullptr)) , m_headerLabels{ i18nc("@title:column", "Task Name"), i18nc("@title:column", "Session Time"), i18nc("@title:column", "Time"), i18nc("@title:column", "Total Session Time"), i18nc("@title:column", "Total Time"), i18nc("@title:column", "Priority"), i18nc("@title:column", "Percent Complete")} , m_clockAnimation(nullptr) , m_dragCutTaskId() { Q_INIT_RESOURCE(pics); m_clockAnimation = new QMovie(":/pics/watch.gif", QByteArray(), this); // Prepare animated icon connect(m_clockAnimation, &QMovie::frameChanged, this, &TasksModel::setActiveIcon); // TODO: stop animation when no tasks are active m_clockAnimation->start(); } void TasksModel::clear() { beginResetModel(); // Empty "m_children", move it to "children". QList children; children.swap(m_rootItem->m_children); for (int i = 0; i < children.count(); ++i) { TasksModelItem *item = children.at(i); item->m_parent = nullptr; delete item; } endResetModel(); } int TasksModel::topLevelItemCount() const { return m_rootItem->childCount(); } int TasksModel::indexOfTopLevelItem(TasksModelItem *item) const { return m_rootItem->m_children.indexOf(item); } TasksModelItem *TasksModel::topLevelItem(int index) const { return m_rootItem->child(index); } TasksModelItem *TasksModel::takeTopLevelItem(int index) { return m_rootItem->takeChild(index); } TasksModelItem *TasksModel::item(const QModelIndex &index) const { if (!index.isValid()) { return nullptr; } return static_cast(index.internalPointer()); } QModelIndex TasksModel::index(TasksModelItem *item, int column) const { if (!item || item == m_rootItem) { return {}; } const TasksModelItem *par = item->parent(); auto *itm = const_cast(item); if (!par) { par = m_rootItem; } int row = par->m_children.lastIndexOf(itm); return createIndex(row, column, itm); } QModelIndex TasksModel::index(int row, int column, const QModelIndex &parent) const { int c = columnCount(parent); if (row < 0 || column < 0 || column >= c) { return {}; } TasksModelItem *parentItem = parent.isValid() ? item(parent) : m_rootItem; if (parentItem && row < parentItem->childCount()) { TasksModelItem *itm = parentItem->child(row); if (itm) { return createIndex(row, column, itm); } return {}; } return {}; } QModelIndex TasksModel::parent(const QModelIndex &child) const { if (!child.isValid()) { return {}; } auto *item = static_cast(child.internalPointer()); if (!item || item == m_rootItem) { return {}; } TasksModelItem *parent = item->parent(); return index(parent, 0); } int TasksModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return m_rootItem->childCount(); } TasksModelItem *parentItem = item(parent); if (parentItem) { return parentItem->childCount(); } return 0; } int TasksModel::columnCount(const QModelIndex& /*parent*/) const { return m_headerLabels.size(); } QVariant TasksModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return {}; } switch (role) { case Qt::TextAlignmentRole: // Align task name: left // Align priority: center // Align HH:MM: right if (index.column() == 5) { return Qt::AlignCenter; } else if (index.column() >= 1) { return {Qt::AlignRight | Qt::AlignVCenter}; } break; case Qt::WhatsThisRole: if (index.column() == 0) { return i18nc("@info:whatsthis", "The task name is what you call the task, it can be chosen freely."); } else if (index.column() == 1) { return i18nc("@info:whatsthis", "The session time is the time since you last chose \"Start New Session\"."); } break; case Qt::DecorationRole: { auto *task = dynamic_cast(item(index)); if (!task) { return {}; } if (index.column() == 0) { return QPixmap(task->isComplete() ? ":/pics/task-complete.xpm" : ":/pics/task-incomplete.xpm"); } else if (index.column() == 1) { return task->isRunning() ? m_clockAnimation->currentPixmap() : QPixmap(":/pics/empty-watch.xpm"); } break; } default: { TasksModelItem *itm = item(index); if (itm) { return itm->data(index.column(), role); } break; } } return {}; } QList TasksModel::getAllItems() { QList res; QStack stack; stack.push(m_rootItem); while (!stack.isEmpty()) { TasksModelItem *item = stack.pop(); if (item != m_rootItem) { res.append(item); } for (int c = 0; c < item->m_children.count(); ++c) { stack.push(item->m_children.at(c)); } } return res; } QList TasksModel::getAllTasks() { QList tasks; for (TasksModelItem *item : getAllItems()) { // If "item" is not a Task, then we are probably in the middle // of class Task or TasksModelItem destructor. Task *task = dynamic_cast(item); if (task) { tasks.append(task); } } return tasks; } QVariant TasksModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section < 0 || orientation != Qt::Horizontal || section >= m_headerLabels.size()) { return {}; } switch (role) { case Qt::DisplayRole: return m_headerLabels[section]; case Qt::WhatsThisRole: switch (section) { case 0: return i18nc("@info:whatsthis", "The task name is what you call the task, it can be chosen freely."); case 1: return i18nc("@info:whatsthis", "The session time is the time since you last chose \"Start New Session\"."); case 3: return i18nc("@info:whatsthis", "The total session time is the session time of this task and all its subtasks."); case 4: return i18nc("@info:whatsthis", "The total time is the time of this task and all its subtasks."); default: break; } break; default: break; } return QAbstractItemModel::headerData(section, orientation, role); } void TasksModel::setActiveIcon(int /*unused*/) { // TODO: only touch those tasks that are currently running for (auto *task : getAllItems()) { task->invalidateRunningState(); } } void TasksModel::addChild(TasksModelItem *item) { m_rootItem->addChild(item); } Qt::ItemFlags TasksModel::flags(const QModelIndex &index) const { if (!index.isValid()) { // Allow move to top-level return Qt::ItemIsDropEnabled; } return QAbstractItemModel::flags(index) | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; } Qt::DropActions TasksModel::supportedDropActions() const { return Qt::MoveAction; } bool TasksModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const { if (m_dragCutTaskId.isEmpty()) { return false; } return QAbstractItemModel::canDropMimeData(data, action, row, column, parent); } bool TasksModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (!canDropMimeData(data, action, row, column, parent)) { return false; } if (action == Qt::IgnoreAction) { return true; } auto *task = taskByUID(m_dragCutTaskId); if (!task) { return false; } // If have parent, move to subtasks of the parent task. // If don't have parent, move to top-level tasks. TasksModelItem *newParent = parent.isValid() ? item(parent) : m_rootItem; if (!newParent) { return false; } task->move(newParent); emit taskDropped(); return true; } QMimeData *TasksModel::mimeData(const QModelIndexList &indexes) const { auto *task = dynamic_cast(item(indexes[0])); if (!task) { return nullptr; } m_dragCutTaskId = task->uid(); return QAbstractItemModel::mimeData(indexes); } Task *TasksModel::taskByUID(const QString &uid) { for (auto *item : getAllItems()) { auto *task = dynamic_cast(item); if (task->uid() == uid) { return task; } } return nullptr; } + +void TasksModel::startNewSession() +{ + for (Task *task : getAllTasks()) { + task->startNewSession(); + } +} diff --git a/src/model/tasksmodel.h b/src/model/tasksmodel.h index da472ad..10f03ac 100644 --- a/src/model/tasksmodel.h +++ b/src/model/tasksmodel.h @@ -1,94 +1,103 @@ /* * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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, see . */ #ifndef KTIMETRACKER_TASKSMODEL_H #define KTIMETRACKER_TASKSMODEL_H #include QT_BEGIN_NAMESPACE class QMovie; QT_END_NAMESPACE class TasksModelItem; class Task; class TasksModel : public QAbstractItemModel { Q_OBJECT friend class TasksModelItem; public: TasksModel(); ~TasksModel() override = default; // Clears the tree widget by removing all of its items and selections. void clear(); int topLevelItemCount() const; int indexOfTopLevelItem(TasksModelItem *item) const; TasksModelItem *topLevelItem(int index) const; TasksModelItem *takeTopLevelItem(int index); QModelIndex index(int row, int column, const QModelIndex &parent) const override; QModelIndex index(TasksModelItem *item, int column) const; QModelIndex parent(const QModelIndex &child) const override; int rowCount(const QModelIndex &parent) const override; int columnCount(const QModelIndex &parent) const override; QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; TasksModelItem *item(const QModelIndex &index) const; QList getAllItems(); QList getAllTasks(); void addChild(TasksModelItem *item); Qt::DropActions supportedDropActions() const override; bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; QMimeData *mimeData(const QModelIndexList &indexes) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; /** return the task with the given UID */ Task *taskByUID(const QString &uid); + /* + * Reset session time to zero for all tasks. + * + * This procedure starts a new session. We speak of session times, + * overalltimes (comprising all sessions) and total times (comprising all subtasks). + * That is why there is also a total session time. + */ + void startNewSession(); + public Q_SLOTS: void setActiveIcon(int frameNumber); Q_SIGNALS: void taskCompleted(Task *task); void taskDropped(); private: TasksModelItem *m_rootItem; QStringList m_headerLabels; QMovie *m_clockAnimation; mutable QString m_dragCutTaskId; }; #endif // KTIMETRACKER_TASKSMODEL_H diff --git a/src/taskview.cpp b/src/taskview.cpp index 72d43c6..c175d18 100644 --- a/src/taskview.cpp +++ b/src/taskview.cpp @@ -1,800 +1,788 @@ /* * 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/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); // Setup manual save timer (to save changes a little while after they happen) m_manualSaveTimer = new QTimer(this); m_manualSaveTimer->setSingleShot( true ); connect(m_manualSaveTimer, &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/ ")); } save(); 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) { delete m_tasksWidget; m_tasksWidget = nullptr; } // 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(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(); if (m_activeTasks.count() == 1) { emit timersActive(); } emit tasksChanged(m_activeTasks); } } 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.")); } } refreshModel(); refreshView(); for (int i = 0; i <= tasksModel->columnCount(QModelIndex()); ++i) { m_tasksWidget->resizeColumnToContents(i); } } void TaskView::closeStorage() { m_storage->closeStorage(); } void TaskView::refreshModel() { for (Task *task : storage()->tasksModel()->getAllTasks()) { task->invalidateCompletedState(); task->update(); // maybe there was a change in the times's format } } void TaskView::refreshView() { m_tasksWidget->refresh(); } /** * Refresh the times of the tasks, e.g. when the history has been changed by the user. * Re-calculate the time for every task based on events in the history. */ QString TaskView::reFreshTimes() { QString err; // This procedure resets all times (session and overall) for all tasks and subtasks. // Reset session and total time for all tasks - do not touch the storage. for (Task *task : storage()->tasksModel()->getAllTasks()) { task->resetTimes(); } for (Task *task : storage()->tasksModel()->getAllTasks()) { // get all events for task for (const auto *event : storage()->eventsModel()->eventsForTask(task)) { QDateTime eventStart = event->dtStart(); QDateTime eventEnd = event->dtEnd(); const int64_t duration = event->duration() / 60; task->addTime(duration); qCDebug(KTT_LOG) << "duration is" << duration; if (task->sessionStartTiMe().isValid()) { // if there is a session if (task->sessionStartTiMe().secsTo(eventStart) > 0 && task->sessionStartTiMe().secsTo(eventEnd) > 0) { // if the event is after the session start task->addSessionTime(duration); } } else { // so there is no session at all task->addSessionTime(duration); } } } // Recalculate total times after changing hierarchy by drag&drop for (Task *task : storage()->tasksModel()->getAllTasks()) { // Start recursive method recalculateTotalTimesSubtree() for each top-level task. if (task->isRoot()) { task->recalculateTotalTimesSubtree(); } } refreshModel(); refreshView(); qCDebug(KTT_LOG) << "Leaving TaskView::reFreshTimes()"; return err; } void TaskView::importPlanner(const QString& fileName) { qCDebug(KTT_LOG) << "entering importPlanner"; auto *handler = new PlannerParser(storage()->projectModel(), m_tasksWidget->currentItem()); QFile xmlFile(fileName); QXmlInputSource source(&xmlFile); QXmlSimpleReader reader; reader.setContentHandler(handler); reader.parse(source); refreshModel(); refreshView(); } void TaskView::scheduleSave() { m_manualSaveTimer->start(10); } 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 && m_activeTasks.indexOf(task) == -1) { if (!task->isComplete()) { if (KTimeTrackerSettings::uniTasking()) { stopAllTimers(); } m_idleTimeDetector->startIdleDetection(); task->setRunning(true, startTime); save(); m_activeTasks.append(task); emit updateButtons(); if (m_activeTasks.count() == 1) { emit timersActive(); } emit tasksChanged(m_activeTasks); } } } 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, m_activeTasks.count(), m_tasksWidget); if (m_activeTasks.count() > 1) { dialog.show(); } for (Task *task : m_activeTasks) { QApplication::processEvents(); task->setRunning(false, when); save(); dialog.setValue(dialog.value() + 1); } m_idleTimeDetector->stopIdleDetection(); m_activeTasks.clear(); emit updateButtons(); emit timersInactive(); emit tasksChanged(m_activeTasks); } 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::startNewSession() -/* This procedure starts a new session. We speak of session times, -overalltimes (comprising all sessions) and total times (comprising all subtasks). -That is why there is also a total session time. */ -{ - qCDebug(KTT_LOG) <<"Entering TaskView::startNewSession"; - for (Task *task : storage()->tasksModel()->getAllTasks()) { - task->startNewSession(); - } - qCDebug(KTT_LOG) << "Leaving TaskView::startNewSession"; -} - void TaskView::resetTimeForAllTasks() /* This procedure resets all times (session and overall) for all tasks and subtasks. */ { qCDebug(KTT_LOG) << "Entering function"; for (Task *task : storage()->tasksModel()->getAllTasks()) { task->resetTimes(); } storage()->deleteAllEvents(); qCDebug(KTT_LOG) << "Leaving function"; } void TaskView::stopTimerFor(Task* task) { qCDebug(KTT_LOG) << "Entering function"; if (task != nullptr && m_activeTasks.indexOf(task) != -1) { m_activeTasks.removeAll(task); task->setRunning(false); save(); if (m_activeTasks.count() == 0) { m_idleTimeDetector->stopIdleDetection(); emit timersInactive(); } emit updateButtons(); } emit tasksChanged(m_activeTasks); } void TaskView::stopCurrentTimer() { stopTimerFor(m_tasksWidget->currentItem()); if (m_focusTrackingActive && m_lastTaskWithFocus == m_tasksWidget->currentItem()) { toggleFocusTracking(); } } void TaskView::minuteUpdate() { addTimeToActiveTasks(1); } void TaskView::addTimeToActiveTasks(int64_t minutes) { for (Task *task : m_activeTasks) { task->changeTime(minutes, nullptr); } scheduleSave(); } void TaskView::newTask() { newTask(i18nc("@title:window", "New Task"), nullptr); } 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); save(); 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); refreshModel(); refreshView(); } 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::editTaskTime() { qCDebug(KTT_LOG) <<"Entering editTask"; Task* task = m_tasksWidget->currentItem(); if (!task) { return; } QPointer editTimeDialog = new EditTimeDialog( m_tasksWidget->parentWidget(), task->name(), task->description(), static_cast(task->time())); if (editTimeDialog->exec() == QDialog::Accepted) { if (editTimeDialog->editHistoryRequested()) { editHistory(); } else { editTaskTime(task->uid(), editTimeDialog->changeMinutes()); } } delete editTimeDialog; } void TaskView::editHistory() { QPointer dialog = new HistoryDialog(m_tasksWidget->parentWidget(), storage()->projectModel()); dialog->exec(); } 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(m_storage); deleteEntry(uid); // forget if the item was expanded or collapsed // Stop idle detection if no more counters are running if (m_activeTasks.count() == 0) { m_idleTimeDetector->stopIdleDetection(); emit timersInactive(); } task->delete_recursive(); emit tasksChanged(m_activeTasks); } 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) { 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(); } refreshModel(); } //---------------------------------------------------------------------------- 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()); scheduleSave(); } } 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 DesktopList desktopList; m_desktopTracker->registerForDesktops(task, desktopList); m_activeTasks.removeAll(task); emit tasksChanged(m_activeTasks); } diff --git a/src/taskview.h b/src/taskview.h index 859f35a..e3345d0 100644 --- a/src/taskview.h +++ b/src/taskview.h @@ -1,239 +1,236 @@ /* * 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; //BEGIN model specified /** Load the view from storage. */ void load(const QUrl& url); /** Close the storage and release lock. */ void closeStorage(); - /** Reset session time to zero for all tasks. */ - void startNewSession(); - /** Reset session and total time to zero for all tasks. */ void resetTimeForAllTasks(); /** Schedule that we should save very soon */ void scheduleSave(); /** 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); //END //BEGIN behavior /** Returns whether the focus tracking is currently active. */ bool isFocusTrackingActive() const; //END 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(); /** Calls newTask dialog with caption "New Task". */ void newTask(); /** 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 refresh (e.g. after import) */ void refreshModel(); void refreshView(); /** 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(); /** Calls editTaskTime dialog for the current task. */ void editTaskTime(); void editHistory(); /** * 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); /** 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; QTimer *m_manualSaveTimer; DesktopTracker* m_desktopTracker; TimeTrackerStorage *m_storage; bool m_focusTrackingActive; Task* m_lastTaskWithFocus; QList m_activeTasks; FocusDetector *m_focusDetector; TasksWidget *m_tasksWidget; private: /** * Add time delta to all active tasks. * Does not modify events model. */ void addTimeToActiveTasks(int64_t minutes); 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/timetrackerwidget.cpp b/src/timetrackerwidget.cpp index aef7d98..e497394 100644 --- a/src/timetrackerwidget.cpp +++ b/src/timetrackerwidget.cpp @@ -1,984 +1,984 @@ /* * Copyright (C) 2007 by Mathias Soeken * 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 "timetrackerwidget.h" #include #include #include #include #include #include #include "dialogs/exportdialog.h" #include "export/export.h" #include "dialogs/historydialog.h" #include "idletimedetector.h" #include "ktimetracker-version.h" #include "ktimetracker.h" #include "ktimetrackerutility.h" #include "ktt_debug.h" #include "mainwindow.h" #include "model/eventsmodel.h" #include "model/projectmodel.h" #include "model/task.h" #include "model/tasksmodel.h" #include "reportcriteria.h" #include "settings/ktimetrackerconfigdialog.h" #include "taskview.h" #include "widgets/searchline.h" #include "widgets/taskswidget.h" TimeTrackerWidget::TimeTrackerWidget(QWidget *parent) : QWidget(parent) , m_searchLine(nullptr) , m_taskView(nullptr) , m_actionCollection(nullptr) { registerDBus(); QLayout* layout = new QVBoxLayout; layout->setMargin(0); layout->setSpacing(0); setLayout(layout); fillLayout(nullptr); } void TimeTrackerWidget::fillLayout(TasksWidget *tasksWidget) { // Remove all items from layout QLayoutItem *child; while ((child = layout()->takeAt(0)) != nullptr) { delete child->widget(); delete child; } if (tasksWidget) { m_searchLine = new SearchLine(this); connect(m_searchLine, &SearchLine::textSubmitted, this, &TimeTrackerWidget::slotAddTask); connect(m_searchLine, &SearchLine::searchUpdated, tasksWidget, &TasksWidget::setFilterText); layout()->addWidget(m_searchLine); layout()->addWidget(tasksWidget); showSearchBar(!KTimeTrackerSettings::configPDA() && KTimeTrackerSettings::showSearchBar()); } else { auto *placeholderWidget = new QWidget(this); placeholderWidget->setBackgroundRole(QPalette::Dark); placeholderWidget->setAutoFillBackground(true); layout()->addWidget(placeholderWidget); } } int TimeTrackerWidget::focusSearchBar() { if (m_searchLine->isEnabled()) { m_searchLine->setFocus(); } return 0; } void TimeTrackerWidget::addTaskView(const QUrl &url) { qCDebug(KTT_LOG) << "Entering function (url=" << url << ")"; closeFile(); m_taskView = new TaskView(this); connect(m_taskView, &TaskView::contextMenuRequested, this, &TimeTrackerWidget::contextMenuRequested); connect(m_taskView, &TaskView::timersActive, this, &TimeTrackerWidget::timersActive); connect(m_taskView, &TaskView::timersInactive, this, &TimeTrackerWidget::timersInactive); connect(m_taskView, &TaskView::tasksChanged, this, &TimeTrackerWidget::tasksChanged); emit setCaption(url.toString()); m_taskView->load(url); fillLayout(m_taskView->tasksWidget()); emit currentTaskViewChanged(); slotCurrentChanged(); } TaskView* TimeTrackerWidget::currentTaskView() const { return m_taskView; } Task* TimeTrackerWidget::currentTask() { auto *taskView = currentTaskView(); if (taskView == nullptr) { return nullptr; } auto *tasksWidget = taskView->tasksWidget(); if (tasksWidget == nullptr) { return nullptr; } return tasksWidget->currentItem(); } void TimeTrackerWidget::setupActions(KActionCollection* actionCollection) { Q_INIT_RESOURCE(pics); m_actionCollection = actionCollection; KStandardAction::open(this, &TimeTrackerWidget::openFileDialog, actionCollection); KStandardAction::save(this, &TimeTrackerWidget::saveFile, actionCollection); KStandardAction::preferences(this, &TimeTrackerWidget::showSettingsDialog, actionCollection); QAction* startNewSession = actionCollection->addAction(QStringLiteral("start_new_session")); startNewSession->setText(i18nc("@action:inmenu", "Start &New Session")); startNewSession->setToolTip(i18nc("@info:tooltip", "Starts a new session")); startNewSession->setWhatsThis(i18nc( "@info:whatsthis", "This will reset the " "session time to 0 for all tasks, to start a new session, without " "affecting the totals.")); connect(startNewSession, &QAction::triggered, this, &TimeTrackerWidget::startNewSession); QAction* editHistory = actionCollection->addAction(QStringLiteral("edit_history")); editHistory->setText(i18nc("@action:inmenu", "Edit History...")); editHistory->setToolTip(i18nc("@info:tooltip", "Edits history of all tasks of the current document")); editHistory->setWhatsThis(i18nc( "@info:whatsthis", "A window will " "be opened where you can change start and stop times of tasks or add a " "comment to them.")); editHistory->setIcon(QIcon::fromTheme("view-history")); connect(editHistory, &QAction::triggered, this, &TimeTrackerWidget::editHistory); QAction* resetAllTimes = actionCollection->addAction(QStringLiteral("reset_all_times")); resetAllTimes->setText(i18nc("@action:inmenu", "&Reset All Times")); resetAllTimes->setToolTip(i18nc("@info:tooltip", "Resets all times")); resetAllTimes->setWhatsThis(i18nc( "@info:whatsthis", "This will reset the session " "and total time to 0 for all tasks, to restart from scratch.")); connect(resetAllTimes, &QAction::triggered, this, &TimeTrackerWidget::resetAllTimes); QAction* startCurrentTimer = actionCollection->addAction(QStringLiteral("start")); startCurrentTimer->setText(i18nc("@action:inmenu", "&Start")); startCurrentTimer->setToolTip(i18nc("@info:tooltip", "Starts timing for selected task")); startCurrentTimer->setWhatsThis(i18nc( "@info:whatsthis", "This will start timing for the " "selected task.\nIt is even possible to time several tasks " "simultaneously.\n\nYou may also start timing of tasks by double clicking " "the left mouse button on a given task. This will, however, stop timing " "of other tasks.")); startCurrentTimer->setIcon(QIcon::fromTheme("media-playback-start")); actionCollection->setDefaultShortcut(startCurrentTimer, QKeySequence(Qt::Key_G)); connect(startCurrentTimer, &QAction::triggered, this, &TimeTrackerWidget::startCurrentTimer); QAction* stopCurrentTimer = actionCollection->addAction(QStringLiteral("stop")); stopCurrentTimer->setText(i18nc("@action:inmenu", "S&top")); stopCurrentTimer->setToolTip(i18nc("@info:tooltip", "Stops timing of the selected task")); stopCurrentTimer->setWhatsThis(i18nc("@info:whatsthis", "Stops timing of the selected task")); stopCurrentTimer->setIcon(QIcon::fromTheme("media-playback-stop")); connect(stopCurrentTimer, &QAction::triggered, this, &TimeTrackerWidget::stopCurrentTimer); QAction* focusSearchBar = actionCollection->addAction(QStringLiteral("focusSearchBar")); focusSearchBar->setText(i18nc("@action:inmenu", "Focus on Searchbar")); focusSearchBar->setToolTip(i18nc("@info:tooltip", "Sets the focus on the searchbar")); focusSearchBar->setWhatsThis(i18nc("@info:whatsthis", "Sets the focus on the searchbar")); actionCollection->setDefaultShortcut(focusSearchBar, QKeySequence(Qt::Key_S)); connect(focusSearchBar, &QAction::triggered, this, &TimeTrackerWidget::focusSearchBar); QAction* stopAllTimers = actionCollection->addAction(QStringLiteral("stopAll")); stopAllTimers->setText(i18nc("@action:inmenu", "Stop &All Timers")); stopAllTimers->setToolTip(i18nc("@info:tooltip", "Stops all of the active timers")); stopAllTimers->setWhatsThis(i18nc("@info:whatsthis", "Stops all of the active timers")); actionCollection->setDefaultShortcut(stopAllTimers, QKeySequence(Qt::Key_Escape)); connect(stopAllTimers, &QAction::triggered, this, &TimeTrackerWidget::stopAllTimers); QAction* focusTracking = actionCollection->addAction(QStringLiteral("focustracking")); focusTracking->setCheckable(true); focusTracking->setText(i18nc("@action:inmenu", "Track Active Applications")); focusTracking->setToolTip(i18nc( "@info:tooltip", "Auto-creates and updates tasks when the focus of the current window has changed")); focusTracking->setWhatsThis(i18nc( "@info:whatsthis", "If the focus of a window changes for the " "first time when this action is enabled, a new task will be created " "with the title of the window as its name and will be started. If there " "already exists such an task it will be started.")); connect(focusTracking, &QAction::triggered, this, &TimeTrackerWidget::focusTracking); QAction* newTask = actionCollection->addAction(QStringLiteral("new_task")); newTask->setText(i18nc("@action:inmenu", "&New Task...")); newTask->setToolTip(i18nc("@info:tooltip", "Creates new top level task")); newTask->setWhatsThis(i18nc("@info:whatsthis", "This will create a new top level task.")); newTask->setIcon(QIcon::fromTheme("document-new")); actionCollection->setDefaultShortcut(newTask, QKeySequence(Qt::CTRL + Qt::Key_T)); connect(newTask, &QAction::triggered, this, &TimeTrackerWidget::newTask); QAction* newSubTask = actionCollection->addAction(QStringLiteral("new_sub_task")); newSubTask->setText(i18nc("@action:inmenu", "New &Subtask...")); newSubTask->setToolTip(i18nc("@info:tooltip", "Creates a new subtask to the current selected task")); newSubTask->setWhatsThis(i18nc("@info:whatsthis", "This will create a new subtask to the current selected task.")); newSubTask->setIcon(QIcon::fromTheme("subtask-new-ktimetracker")); actionCollection->setDefaultShortcut(newSubTask, QKeySequence(Qt::CTRL + Qt::Key_B)); connect(newSubTask, &QAction::triggered, this, &TimeTrackerWidget::newSubTask); QAction* deleteTask = actionCollection->addAction(QStringLiteral("delete_task")); deleteTask->setText(i18nc("@action:inmenu", "&Delete")); deleteTask->setToolTip(i18nc("@info:tooltip", "Deletes selected task")); deleteTask->setWhatsThis(i18nc("@info:whatsthis", "This will delete the selected task(s) and all subtasks.")); deleteTask->setIcon(QIcon::fromTheme("edit-delete")); actionCollection->setDefaultShortcut(deleteTask, QKeySequence(Qt::Key_Delete)); connect(deleteTask, &QAction::triggered, this, QOverload<>::of(&TimeTrackerWidget::deleteTask)); QAction* editTask = actionCollection->addAction(QStringLiteral("edit_task")); editTask->setText(i18nc("@action:inmenu", "&Properties")); editTask->setToolTip(i18nc("@info:tooltip", "Edit name or description for selected task")); editTask->setWhatsThis(i18nc( "@info:whatsthis", "This will bring up a dialog " "box where you may edit the parameters for the selected task.")); editTask->setIcon(QIcon::fromTheme("document-properties")); actionCollection->setDefaultShortcut(editTask, QKeySequence(Qt::CTRL + Qt::Key_E)); connect(editTask, &QAction::triggered, this, &TimeTrackerWidget::editTask); QAction* editTaskTime = actionCollection->addAction(QStringLiteral("edit_task_time")); editTaskTime->setText(i18nc("@action:inmenu", "Edit &Time...")); editTaskTime->setToolTip(i18nc("@info:tooltip", "Edit time for selected task")); editTaskTime->setWhatsThis(i18nc( "@info:whatsthis", "This will bring up a dialog " "box where you may edit the times for the selected task.")); editTaskTime->setIcon(QIcon::fromTheme("document-edit")); actionCollection->setDefaultShortcut(editTaskTime, QKeySequence(Qt::Key_E)); connect(editTaskTime, &QAction::triggered, this, &TimeTrackerWidget::editTaskTime); QAction* markTaskAsComplete = actionCollection->addAction(QStringLiteral("mark_as_complete")); markTaskAsComplete->setText(i18nc("@action:inmenu", "&Mark as Complete")); markTaskAsComplete->setIcon(QPixmap(":/pics/task-complete.xpm")); actionCollection->setDefaultShortcut(markTaskAsComplete, QKeySequence(Qt::CTRL + Qt::Key_M)); connect(markTaskAsComplete, &QAction::triggered, this, &TimeTrackerWidget::markTaskAsComplete); QAction* markTaskAsIncomplete = actionCollection->addAction(QStringLiteral("mark_as_incomplete")); markTaskAsIncomplete->setText(i18nc("@action:inmenu", "&Mark as Incomplete")); markTaskAsIncomplete->setIcon(QPixmap(":/pics/task-incomplete.xpm")); actionCollection->setDefaultShortcut(markTaskAsIncomplete, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_M)); connect(markTaskAsIncomplete, &QAction::triggered, this, &TimeTrackerWidget::markTaskAsIncomplete); QAction* exportAction = actionCollection->addAction(QStringLiteral("export_dialog")); exportAction->setText(i18nc("@action:inmenu", "&Export...")); exportAction->setIcon(QIcon::fromTheme("document-export")); connect(exportAction, &QAction::triggered, this, &TimeTrackerWidget::exportDialog); QAction* importPlanner = actionCollection->addAction(QStringLiteral("import_planner")); importPlanner->setText(i18nc("@action:inmenu", "Import Tasks From &Planner...")); importPlanner->setIcon(QIcon::fromTheme("document-import")); connect(importPlanner, &QAction::triggered, [=]() { const QString &fileName = QFileDialog::getOpenFileName(); importPlannerFile(fileName); }); QAction* showSearchBar = actionCollection->addAction(QStringLiteral("searchbar")); showSearchBar->setCheckable(true); showSearchBar->setChecked(KTimeTrackerSettings::showSearchBar()); showSearchBar->setText(i18nc("@action:inmenu", "Show Searchbar")); connect(showSearchBar, &QAction::triggered, this, &TimeTrackerWidget::slotSearchBar); connect(this, &TimeTrackerWidget::currentTaskChanged, this, &TimeTrackerWidget::slotUpdateButtons); connect(this, &TimeTrackerWidget::currentTaskViewChanged, this, &TimeTrackerWidget::slotUpdateButtons); connect(this, &TimeTrackerWidget::updateButtons, this, &TimeTrackerWidget::slotUpdateButtons); slotUpdateButtons(); } QAction *TimeTrackerWidget::action(const QString &name) const { return m_actionCollection->action(name); } void TimeTrackerWidget::openFile(const QUrl &url) { qCDebug(KTT_LOG) << "Entering function, url is " << url; addTaskView(url); } void TimeTrackerWidget::openFileDialog() { const QString &path = QFileDialog::getOpenFileName(this); if (!path.isEmpty()) { openFile(QUrl::fromLocalFile(path)); } } bool TimeTrackerWidget::closeFile() { qCDebug(KTT_LOG) << "Entering TimeTrackerWidget::closeFile"; TaskView* taskView = currentTaskView(); if (taskView) { taskView->save(); taskView->closeStorage(); } emit currentTaskViewChanged(); emit setCaption(QString()); slotCurrentChanged(); delete taskView; // removeTab does not delete its widget. m_taskView = nullptr; return true; } void TimeTrackerWidget::saveFile() { currentTaskView()->save(); } void TimeTrackerWidget::showSearchBar(bool visible) { m_searchLine->setVisible(visible); } bool TimeTrackerWidget::closeAllFiles() { qCDebug(KTT_LOG) << "Entering TimeTrackerWidget::closeAllFiles"; bool err = true; if (m_taskView) { m_taskView->stopAllTimers(); err = closeFile(); } return err; } void TimeTrackerWidget::slotCurrentChanged() { qDebug() << "entering KTimeTrackerWidget::slotCurrentChanged"; if (m_taskView) { connect(m_taskView, &TaskView::updateButtons, this, &TimeTrackerWidget::updateButtons, Qt::UniqueConnection); connect(m_taskView, &TaskView::setStatusBarText, this, &TimeTrackerWidget::statusBarTextChangeRequested, Qt::UniqueConnection); connect(m_taskView, &TaskView::timersActive, this, &TimeTrackerWidget::timersActive, Qt::UniqueConnection); connect(m_taskView, &TaskView::timersInactive, this, &TimeTrackerWidget::timersInactive, Qt::UniqueConnection); connect(m_taskView, &TaskView::tasksChanged, this, &TimeTrackerWidget::tasksChanged, Qt::UniqueConnection); emit setCaption(m_taskView->storage()->fileUrl().toString()); } } void TimeTrackerWidget::slotAddTask(const QString &taskName) { TaskView *taskView = currentTaskView(); taskView->addTask(taskName, QString(), 0, 0, DesktopList(), nullptr); taskView->save(); } void TimeTrackerWidget::slotUpdateButtons() { Task *item = currentTask(); action(QStringLiteral("start"))->setEnabled(item && !item->isRunning() && !item->isComplete()); action(QStringLiteral("stop"))->setEnabled(item && item->isRunning()); action(QStringLiteral("delete_task"))->setEnabled(item); action(QStringLiteral("edit_task_time"))->setEnabled(item); action(QStringLiteral("edit_task"))->setEnabled(item); action(QStringLiteral("mark_as_complete"))->setEnabled(item && !item->isComplete()); action(QStringLiteral("mark_as_incomplete"))->setEnabled(item && item->isComplete()); action(QStringLiteral("new_task"))->setEnabled(currentTaskView()); action(QStringLiteral("new_sub_task"))->setEnabled(currentTaskView() && currentTaskView()->storage()->isLoaded() && currentTaskView()->storage()->tasksModel()->getAllTasks().size()); action(QStringLiteral("focustracking"))->setEnabled(currentTaskView()); action(QStringLiteral("focustracking"))->setChecked(currentTaskView() && currentTaskView()->isFocusTrackingActive()); action(QStringLiteral("start_new_session"))->setEnabled(currentTaskView()); action(QStringLiteral("edit_history"))->setEnabled(currentTaskView()); action(QStringLiteral("reset_all_times"))->setEnabled(currentTaskView()); action(QStringLiteral("export_dialog"))->setEnabled(currentTaskView()); action(QStringLiteral("import_planner"))->setEnabled(currentTaskView()); action(QStringLiteral("file_save"))->setEnabled(currentTaskView()); } void TimeTrackerWidget::showSettingsDialog() { if (KConfigDialog::showDialog("settings")) { return; } auto *dialog = new KConfigDialog(this, "settings", KTimeTrackerSettings::self()); dialog->setFaceType(KPageDialog::List); dialog->addPage(new KTimeTrackerBehaviorConfig(dialog), i18nc("@title:tab", "Behavior"), QStringLiteral("preferences-other")); dialog->addPage(new KTimeTrackerDisplayConfig(dialog), i18nc("@title:tab", "Appearance"), QStringLiteral("preferences-desktop-theme")); dialog->addPage(new KTimeTrackerStorageConfig(dialog), i18nc("@title:tab", "Storage"), QStringLiteral("system-file-manager")); connect(dialog, &KConfigDialog::settingsChanged, this, &TimeTrackerWidget::loadSettings); dialog->show(); } void TimeTrackerWidget::loadSettings() { KTimeTrackerSettings::self()->load(); showSearchBar(!KTimeTrackerSettings::configPDA() && KTimeTrackerSettings::showSearchBar()); currentTaskView()->reconfigureModel(); currentTaskView()->tasksWidget()->reconfigure(); } //BEGIN wrapper slots void TimeTrackerWidget::startCurrentTimer() { currentTaskView()->startCurrentTimer(); } void TimeTrackerWidget::stopCurrentTimer() { currentTaskView()->stopCurrentTimer(); } void TimeTrackerWidget::stopAllTimers() { currentTaskView()->stopAllTimers(QDateTime::currentDateTime()); } void TimeTrackerWidget::newTask() { currentTaskView()->newTask(); } void TimeTrackerWidget::newSubTask() { currentTaskView()->newSubTask(); } void TimeTrackerWidget::editTask() { currentTaskView()->editTask(); } void TimeTrackerWidget::editTaskTime() { currentTaskView()->editTaskTime(); } void TimeTrackerWidget::deleteTask() { currentTaskView()->deleteTask(); currentTaskView()->save(); } void TimeTrackerWidget::markTaskAsComplete() { currentTaskView()->markTaskAsComplete(); currentTaskView()->save(); } void TimeTrackerWidget::markTaskAsIncomplete() { currentTaskView()->markTaskAsIncomplete(); currentTaskView()->save(); } void TimeTrackerWidget::exportDialog() { qCDebug(KTT_LOG) << "TimeTrackerWidget::exportDialog()"; auto *taskView = currentTaskView(); ExportDialog dialog(taskView->tasksWidget(), taskView); if (taskView->tasksWidget()->currentItem() && taskView->tasksWidget()->currentItem()->isRoot()) { dialog.enableTasksToExportQuestion(); } dialog.exec(); } void TimeTrackerWidget::startNewSession() { - currentTaskView()->startNewSession(); + currentTaskView()->storage()->tasksModel()->startNewSession(); } void TimeTrackerWidget::editHistory() { // HistoryDialog is the new HistoryDialog, but the EditHiStoryDiaLog exists as well. // HistoryDialog can be edited with qtcreator and qtdesigner, EditHiStoryDiaLog cannot. if (currentTaskView()) { QPointer dialog = new HistoryDialog( currentTaskView()->tasksWidget(), currentTaskView()->storage()->projectModel()); if (currentTaskView()->storage()->eventsModel()->events().count() != 0) { dialog->exec(); } else { KMessageBox::information(nullptr, i18nc("@info in message box", "There is no history yet. Start and stop a task and you will have an entry in your history.")); } } } void TimeTrackerWidget::resetAllTimes() { if (currentTaskView()) { if (KMessageBox::warningContinueCancel( this, i18n("Do you really want to reset the time to zero for all tasks? This will delete the entire history."), i18nc("@title:window", "Confirmation Required"), KGuiItem(i18nc("@action:button", "Reset All Times"))) == KMessageBox::Continue) { currentTaskView()->resetTimeForAllTasks(); } } } void TimeTrackerWidget::focusTracking() { currentTaskView()->toggleFocusTracking(); action(QStringLiteral("focustracking"))->setChecked(currentTaskView()->isFocusTrackingActive()); } void TimeTrackerWidget::slotSearchBar() { bool currentVisible = KTimeTrackerSettings::showSearchBar(); KTimeTrackerSettings::setShowSearchBar(!currentVisible); action(QStringLiteral("searchbar"))->setChecked(!currentVisible); showSearchBar(!currentVisible); } //END /** \defgroup dbus slots ‘‘dbus slots’’ */ /* @{ */ #include "mainadaptor.h" void TimeTrackerWidget::registerDBus() { new MainAdaptor(this); QDBusConnection::sessionBus().registerObject("/KTimeTracker", this); } QString TimeTrackerWidget::version() const { return KTIMETRACKER_VERSION_STRING; } QStringList TimeTrackerWidget::taskIdsFromName( const QString &taskName ) const { QStringList result; TaskView *taskView = currentTaskView(); if (!taskView) { return result; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->name() == taskName) { result << task->uid(); } } return result; } void TimeTrackerWidget::addTask( const QString &taskName ) { TaskView *taskView = currentTaskView(); if (taskView) { taskView->addTask(taskName, QString(), 0, 0, DesktopList(), nullptr); taskView->save(); } } void TimeTrackerWidget::addSubTask(const QString &taskName, const QString &taskId) { TaskView *taskView = currentTaskView(); if (taskView) { taskView->addTask(taskName, QString(), 0, 0, DesktopList(), taskView->storage()->tasksModel()->taskByUID(taskId)); taskView->refreshModel(); taskView->refreshView(); taskView->save(); } } void TimeTrackerWidget::deleteTask(const QString &taskId) { TaskView *taskView = currentTaskView(); if (!taskView) { return; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->uid() == taskId) { taskView->deleteTaskBatch(task); taskView->save(); break; } } } void TimeTrackerWidget::setPercentComplete(const QString &taskId, int percent) { TaskView *taskView = currentTaskView(); if (!taskView) { return; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->uid() == taskId) { task->setPercentComplete(percent); } } } int TimeTrackerWidget::bookTime(const QString &taskId, const QString &dateTime, int64_t minutes) { QDate startDate; QTime startTime; QDateTime startDateTime; if (minutes <= 0) { return KTIMETRACKER_ERR_INVALID_DURATION; } Task *task = nullptr; TaskView *taskView = currentTaskView(); if (taskView) { for (Task *t : taskView->storage()->tasksModel()->getAllTasks()) { if (t->uid() == taskId) { task = t; break; } } } if (!task) { return KTIMETRACKER_ERR_UID_NOT_FOUND; } // Parse datetime startDate = QDate::fromString(dateTime, Qt::ISODate); if (dateTime.length() > 10) { // "YYYY-MM-DD".length() = 10 startTime = QTime::fromString(dateTime, Qt::ISODate); } else { startTime = QTime(12, 0); } if (startDate.isValid() && startTime.isValid()) { startDateTime = QDateTime(startDate, startTime); } else { return KTIMETRACKER_ERR_INVALID_DATE; } // Update task totals (session and total) and save to disk task->changeTotalTimes(task->sessionTime() + minutes, task->totalTime() + minutes); if (!taskView->storage()->bookTime(task, startDateTime, minutes * 60)) { return KTIMETRACKER_ERR_GENERIC_SAVE_FAILED; } return 0; } int TimeTrackerWidget::changeTime(const QString &taskId, int64_t minutes) { if (minutes <= 0) { return KTIMETRACKER_ERR_INVALID_DURATION; } // Find task TaskView *taskView = currentTaskView(); if (!taskView) { //FIXME: it mimics the behaviour with the for loop, but I am not sure semantics were right. Maybe a new error code must be defined? return KTIMETRACKER_ERR_UID_NOT_FOUND; } Task *task = nullptr; for (Task *t : taskView->storage()->tasksModel()->getAllTasks()) { if (t->uid() == taskId) { task = t; break; } } if (!task) { return KTIMETRACKER_ERR_UID_NOT_FOUND; } task->changeTime(minutes, taskView->storage()->eventsModel()); taskView->scheduleSave(); return 0; } QString TimeTrackerWidget::error( int errorCode ) const { switch ( errorCode ) { case KTIMETRACKER_ERR_GENERIC_SAVE_FAILED: return i18n( "Save failed, most likely because the file could not be locked." ); case KTIMETRACKER_ERR_COULD_NOT_MODIFY_RESOURCE: return i18n( "Could not modify calendar resource." ); case KTIMETRACKER_ERR_MEMORY_EXHAUSTED: return i18n( "Out of memory--could not create object." ); case KTIMETRACKER_ERR_UID_NOT_FOUND: return i18n( "UID not found." ); case KTIMETRACKER_ERR_INVALID_DATE: return i18n( "Invalidate date--format is YYYY-MM-DD." ); case KTIMETRACKER_ERR_INVALID_TIME: return i18n( "Invalid time--format is YYYY-MM-DDTHH:MM:SS." ); case KTIMETRACKER_ERR_INVALID_DURATION: return i18n( "Invalid task duration--must be greater than zero." ); default: return i18n( "Invalid error number: %1", errorCode ); } } bool TimeTrackerWidget::isIdleDetectionPossible() const { return IdleTimeDetector::isIdleDetectionPossible(); } int TimeTrackerWidget::totalMinutesForTaskId(const QString &taskId) const { TaskView *taskView = currentTaskView(); if (!taskView) { return -1; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->uid() == taskId) { return task->totalTime(); } } return -1; } void TimeTrackerWidget::startTimerFor(const QString &taskId) { qDebug(); TaskView *taskView = currentTaskView(); if (!taskView) { return; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->uid() == taskId) { taskView->startTimerForNow(task); return; } } } bool TimeTrackerWidget::startTimerForTaskName( const QString &taskName ) { TaskView *taskView = currentTaskView(); if (!taskView) { return false; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->name() == taskName ) { taskView->startTimerForNow(task); return true; } } return false; } bool TimeTrackerWidget::stopTimerForTaskName(const QString &taskName) { TaskView *taskView = currentTaskView(); if (!taskView) { return false; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->name() == taskName) { taskView->stopTimerFor(task); return true; } } return false; } void TimeTrackerWidget::stopTimerFor(const QString &taskId) { TaskView *taskView = currentTaskView(); if (!taskView) { return; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->uid() == taskId) { taskView->stopTimerFor(task); return; } } } void TimeTrackerWidget::stopAllTimersDBUS() { TaskView *taskView = currentTaskView(); if (taskView) { taskView->stopAllTimers(); } } QString TimeTrackerWidget::exportCSVFile( const QString &filename, const QString &from, const QString &to, int type, bool decimalMinutes, bool allTasks, const QString &delimiter, const QString "e) { TaskView *taskView = currentTaskView(); if (!taskView) { return ""; } ReportCriteria rc; rc.from = QDate::fromString(from); if (rc.from.isNull()) { rc.from = QDate::fromString(from, Qt::ISODate); } rc.to = QDate::fromString(to); if (rc.to.isNull()) { rc.to = QDate::fromString(to, Qt::ISODate); } rc.reportType = static_cast(type); rc.decimalMinutes = decimalMinutes; rc.allTasks = allTasks; rc.delimiter = delimiter; rc.quote = quote; QString output = exportToString(taskView->storage()->projectModel(), taskView->tasksWidget()->currentItem(), rc); return writeExport(output, QUrl::fromLocalFile(filename)); } void TimeTrackerWidget::importPlannerFile(const QString &filename) { TaskView *taskView = currentTaskView(); if (!taskView) { return; } taskView->importPlanner(filename); } bool TimeTrackerWidget::isActive(const QString &taskId) const { TaskView *taskView = currentTaskView(); if (!taskView) { return false; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->uid() == taskId) { return task->isRunning(); } } return false; } bool TimeTrackerWidget::isTaskNameActive(const QString &taskName) const { TaskView *taskView = currentTaskView(); if (!taskView) { return false; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->name() == taskName) { return task->isRunning(); } } return false; } QStringList TimeTrackerWidget::tasks() const { QStringList result; TaskView *taskView = currentTaskView(); if (!taskView) { return result; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { result << task->name(); } return result; } QStringList TimeTrackerWidget::activeTasks() const { QStringList result; TaskView* taskView = currentTaskView(); if (!taskView) { return result; } for (Task *task : taskView->storage()->tasksModel()->getAllTasks()) { if (task->isRunning()) { result << task->name(); } } return result; } void TimeTrackerWidget::saveAll() { currentTaskView()->save(); } void TimeTrackerWidget::quit() { auto* mainWindow = dynamic_cast(parent()->parent()); if (mainWindow) { mainWindow->quit(); } else { qCWarning(KTT_LOG) << "Cast to MainWindow failed"; } } bool TimeTrackerWidget::event(QEvent *event) // inherited from QWidget { if (event->type() == QEvent::QueryWhatsThis) { if (m_taskView->storage()->tasksModel()->getAllTasks().empty()) { setWhatsThis(i18nc( "@info:whatsthis", "This is ktimetracker, KDE's program to help you track your time. " "Best, start with creating your first task - enter it into the field " "where you see \"Search or add task\".")); } else { setWhatsThis(i18nc( "@info:whatsthis", "You have already created a task. You can now start and stop timing.")); } } return QWidget::event(event); } // END of dbus slots group /* @} */