diff --git a/src/edittaskdialog.cpp b/src/edittaskdialog.cpp index 62645b2..f959ead 100644 --- a/src/edittaskdialog.cpp +++ b/src/edittaskdialog.cpp @@ -1,102 +1,94 @@ #include "edittaskdialog.h" #include #include #include -#include "historydialog.h" #include "ktimetrackerutility.h" #include "model/projectmodel.h" #include "model/eventsmodel.h" #include "widgets/taskswidget.h" EditTaskDialog::EditTaskDialog(QWidget *parent, ProjectModel *projectModel, const QString &caption, DesktopList *desktopList) : QDialog(parent) , m_projectModel(projectModel) , m_ui() , m_desktopCheckboxes() { setWindowTitle(caption); m_ui.setupUi(this); // Set the desktop checkboxes const int lines = 5; for (int i = 0; i < KWindowSystem::numberOfDesktops(); ++i) { auto *checkbox = new QCheckBox(m_ui.autotrackinggroupbox); checkbox->setObjectName(QString::fromUtf8("desktop_").append(i)); checkbox->setText(KWindowSystem::desktopName(i + 1)); m_ui.gridLayout_2->addWidget(checkbox, i % lines, i / lines + 1); m_desktopCheckboxes.push_back(checkbox); } if (desktopList && !desktopList->empty()) { for (int desktop : *desktopList) { m_desktopCheckboxes[desktop]->setChecked(true); } m_ui.autotrackinggroupbox->setChecked(true); } else { for (QCheckBox *checkbox : m_desktopCheckboxes) { checkbox->setEnabled(false); } } } void EditTaskDialog::changeEvent(QEvent *e) { QDialog::changeEvent(e); switch (e->type()) { case QEvent::LanguageChange: m_ui.retranslateUi(this); break; default: break; } } QString EditTaskDialog::taskName() { return m_ui.tasknamelineedit->text(); } QString EditTaskDialog::taskDescription() { return m_ui.tasknametextedit->toPlainText(); } void EditTaskDialog::setTask(const QString &name) { m_ui.tasknamelineedit->setText(name); } void EditTaskDialog::setDescription(const QString &description) { m_ui.tasknametextedit->setText(description); } void EditTaskDialog::status(DesktopList *desktopList) const { if (desktopList) { desktopList->clear(); for (int i = 0; i < m_desktopCheckboxes.count(); ++i) { if (m_desktopCheckboxes[i]->isEnabled() && m_desktopCheckboxes[i]->isChecked()) { desktopList->append(i); } } } } -void EditTaskDialog::on_edittimespushbutton_clicked() -{ - auto *dialog = new HistoryDialog(parentWidget(), m_projectModel); - lower(); - dialog->exec(); -} - void EditTaskDialog::on_autotrackinggroupbox_clicked() { for (QCheckBox *checkbox : m_desktopCheckboxes) { checkbox->setEnabled(m_ui.autotrackinggroupbox->isChecked()); } } diff --git a/src/edittaskdialog.h b/src/edittaskdialog.h index 6d88db0..7620cc7 100644 --- a/src/edittaskdialog.h +++ b/src/edittaskdialog.h @@ -1,54 +1,53 @@ #ifndef EDITTASKDIALOG_H #define EDITTASKDIALOG_H #include #include "desktoplist.h" #include "ui_edittaskdialog.h" QT_BEGIN_NAMESPACE class QCheckBox; QT_END_NAMESPACE class ProjectModel; /** * Class to show a dialog in ktimetracker to enter data about a task. * * Holds task name and auto-tracking virtual desktops. * * Typically this class will be called by a taskview object to enter a new task * or to edit an existing task. After quitting, you can read out task name and all * properties till you delete the object. * * @short Class to show a dialog to enter data about a task * @author Thorsten Staerk */ class EditTaskDialog : public QDialog { Q_OBJECT public: EditTaskDialog(QWidget *parent, ProjectModel *projectModel, const QString &caption, DesktopList *desktopList = nullptr); ~EditTaskDialog() override = default; QString taskName(); QString taskDescription(); void setTask(const QString &name); void setDescription(const QString &description); void status(DesktopList *desktopList) const; protected: void changeEvent(QEvent *e) override; private Q_SLOTS: void on_autotrackinggroupbox_clicked(); - void on_edittimespushbutton_clicked(); private: ProjectModel *m_projectModel; Ui::EditTaskDialog m_ui; QVector m_desktopCheckboxes; }; #endif // EDITTASKDIALOG_H diff --git a/src/edittaskdialog.ui b/src/edittaskdialog.ui index 052543c..1598520 100644 --- a/src/edittaskdialog.ui +++ b/src/edittaskdialog.ui @@ -1,150 +1,134 @@ EditTaskDialog 0 0 404 496 Add/Edit a task 0 0 Task Name: 0 0 <p>Enter the name of the task here. You can choose it freely.</p> <p><i>Example:</i> phone with mother</p> Description: 0 0 - + 0 111 392 111 Auto Tracking true false - + Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - 0 - 0 - - - - To change this task's time, you have to edit its event history. - - - Edit Times - - - buttonBox accepted() EditTaskDialog accept() 248 254 157 274 buttonBox rejected() EditTaskDialog reject() 316 260 286 274 diff --git a/src/qml/EditTimeDialog.qml b/src/qml/EditTimeDialog.qml index 592b259..92e8eb5 100644 --- a/src/qml/EditTimeDialog.qml +++ b/src/qml/EditTimeDialog.qml @@ -1,166 +1,191 @@ /* * Copyright 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 . * */ import QtQuick 2.3 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.11 import QtQuick.Window 2.3 import org.kde.kirigami 2.4 as Kirigami Window { id: dialog property int minutes property string taskUid property string taskName property string taskDescription property int changeMinutes: 0 signal changeTime(string taskUid, int minutes) + signal editHistory() visible: false modality: Qt.ApplicationModal title: i18nc("@title:window", "Edit Task Time") minimumWidth: 500 minimumHeight: 330 width: minimumWidth height: minimumHeight onVisibleChanged: { if (visible) { timeChangeField.focus = true; timeChangeField.clear(); } } Component.onCompleted: { setX(Screen.width / 2 - width / 2); setY(Screen.height / 2 - height / 2); } Kirigami.Page { anchors.fill: parent focus: true ColumnLayout { anchors.fill: parent Kirigami.FormLayout { Layout.alignment: Qt.AlignHCenter Text { Kirigami.FormData.label: i18n("Task Name:") text: taskName font.bold: true wrapMode: Text.Wrap } Text { Kirigami.FormData.label: i18n("Task Description:") text: taskDescription wrapMode: Text.Wrap elide: Text.ElideRight maximumLineCount: 5 } Kirigami.Separator { Kirigami.FormData.isSection: true } Text { Kirigami.FormData.label: i18n("Current Time:") text: formatTime(minutes) } RowLayout { TextField { id: timeChangeField Layout.preferredWidth: 100 inputMethodHints: Qt.ImhDigitsOnly readonly property IntValidator intValidator: IntValidator {} onTextChanged: { var parsed = parseInt(timeChangeField.text); dialog.changeMinutes = isNaN(parsed) ? 0 : parsed; if (dialog.changeMinutes <= intValidator.bottom || dialog.changeMinutes >= intValidator.top) { dialog.changeMinutes = 0; } } } Text { text: i18nc("label after text field for minutes", "min") } Kirigami.FormData.label: i18n("Change Time By:") } Text { Kirigami.FormData.label: i18n("Time After Change:") text: formatTime(minutes + changeMinutes) } } Item { Layout.fillHeight: true } - DialogButtonBox { - id: buttonBox - Layout.alignment: Qt.AlignRight - + RowLayout { Button { - id: buttonBoxOk - text: i18n("OK") - icon.name: "dialog-ok" - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole - enabled: dialog.changeMinutes != 0 + id: buttonBoxHistory + text: i18n("Edit History...") + icon.name: "document-edit" + + hoverEnabled: true + ToolTip { + text: i18n("To change this task's time, you have to edit its event history") + visible: parent.hovered + delay: 500 + } + + onClicked: { + dialog.hide(); + dialog.editHistory(); + } } - Button { - id: buttonBoxCancel - text: i18n("Cancel") - icon.name: "dialog-cancel" - DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + Item { + Layout.fillWidth: true } - onAccepted: { - dialog.changeTime(dialog.taskUid, dialog.changeMinutes); - dialog.hide(); + DialogButtonBox { + id: buttonBox + Layout.alignment: Qt.AlignRight + + Button { + id: buttonBoxOk + text: i18n("OK") + icon.name: "dialog-ok" + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + enabled: dialog.changeMinutes != 0 + } + + Button { + id: buttonBoxCancel + text: i18n("Cancel") + icon.name: "dialog-cancel" + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } + + onAccepted: { + dialog.changeTime(dialog.taskUid, dialog.changeMinutes); + dialog.hide(); + } + onRejected: dialog.hide() } - onRejected: dialog.hide() } Keys.onEscapePressed: buttonBoxCancel.clicked() Keys.onReturnPressed: buttonBoxOk.clicked() } } function formatTime(minutes) { var abs = Math.abs(minutes); return "%1%2:%3" .arg(minutes < 0 ? '-' : '') .arg(Math.floor(abs / 60)) .arg(abs % 60 >= 10 ? abs % 60 : "0" + abs % 60); } } diff --git a/src/taskview.cpp b/src/taskview.cpp index 77a2074..d4535c8 100644 --- a/src/taskview.cpp +++ b/src/taskview.cpp @@ -1,802 +1,812 @@ /* * 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 "model/task.h" #include "model/tasksmodel.h" #include "model/eventsmodel.h" #include "widgets/taskswidget.h" #include "csvexportdialog.h" #include "desktoptracker.h" #include "edittaskdialog.h" #include "idletimedetector.h" #include "import/plannerparser.h" #include "ktimetracker.h" #include "export/export.h" #include "treeviewheadercontextmenu.h" #include "focusdetector.h" #include "ktimetrackerutility.h" +#include "historydialog.h" #include "ktt_debug.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); auto* engine = new QQmlApplicationEngine(); engine->load("qrc:/qml/EditTimeDialog.qml"); if (!engine->rootObjects().empty()) { m_editTimeDialog = engine->rootObjects().first(); connect( m_editTimeDialog, SIGNAL(changeTime(const QString&, int)), this, SLOT(editTaskTime(const QString&, int))); + connect( + m_editTimeDialog, SIGNAL(editHistory()), + this, SLOT(editHistory())); } else { m_editTimeDialog = nullptr; } } 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); 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 TreeViewHeaderContextMenu *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")); } refresh(); } for (int i = 0; i <= tasksModel->columnCount(QModelIndex()); ++i) { m_tasksWidget->resizeColumnToContents(i); } } void TaskView::closeStorage() { m_storage->closeStorage(); } bool TaskView::allEventsHaveEndTiMe() { return m_storage->allEventsHaveEndTime(); } void TaskView::refresh() { if (!m_tasksWidget) { return; } qCDebug(KTT_LOG) << "entering function"; for (Task *task : storage()->tasksModel()->getAllTasks()) { task->invalidateCompletedState(); task->update(); // maybe there was a change in the times's format } // remove root decoration if there is no more child. // int i = 0; // while (itemAt(++i) && itemAt(i)->depth() == 0){}; //setRootIsDecorated( itemAt( i ) && ( itemAt( i )->depth() != 0 ) ); // FIXME workaround? seems that the QItemDelegate for the procent column only // works properly if rootIsDecorated == true. m_tasksWidget->setRootIsDecorated(true); emit updateButtons(); qCDebug(KTT_LOG) << "exiting TaskView::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 int 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(); } } refresh(); 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); refresh(); } 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); 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(i18n("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); 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); 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, false); } void TaskView::addTimeToActiveTasks(int minutes, bool save_data) { for (Task *task : m_activeTasks) { task->changeTime(minutes, save_data ? m_storage->eventsModel() : nullptr); } scheduleSave(); } void TaskView::newTask() { newTask(i18n("New Task"), nullptr); } void TaskView::newTask(const QString &caption, Task *parent) { auto *dialog = new EditTaskDialog(m_tasksWidget->parentWidget(), storage()->projectModel(), caption, nullptr); DesktopList desktopList; int result = dialog->exec(); if (result == QDialog::Accepted) { QString taskName = i18n("Unnamed Task"); if (!dialog->taskName().isEmpty()) { taskName = dialog->taskName(); } QString taskDescription = dialog->taskDescription(); dialog->status(&desktopList); // 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(); } long total = 0; long 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/")); } } emit updateButtons(); } Task *TaskView::addTask( const QString& taskname, const QString& taskdescription, long total, long 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(i18n("New Sub Task"), task); m_tasksWidget->setExpanded(m_filterProxyModel->mapFromSource(storage()->tasksModel()->index(task, 0)), true); refresh(); } void TaskView::editTask() { qCDebug(KTT_LOG) <<"Entering editTask"; Task* task = m_tasksWidget->currentItem(); if (!task) { return; } DesktopList desktopList = task->desktops(); DesktopList oldDeskTopList = desktopList; auto *dialog = new EditTaskDialog(m_tasksWidget->parentWidget(), storage()->projectModel(), i18n("Edit Task"), &desktopList); dialog->setTask(task->name()); dialog->setDescription(task->description()); int result = dialog->exec(); if (result == QDialog::Accepted) { QString taskName = i18n("Unnamed Task"); if (!dialog->taskName().isEmpty()) { taskName = dialog->taskName(); } // setName only does something if the new name is different task->setName(taskName); task->setDescription(dialog->taskDescription()); dialog->status(&desktopList); // 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(); } } void TaskView::editTaskTime() { qCDebug(KTT_LOG) <<"Entering editTask"; Task* task = m_tasksWidget->currentItem(); if (!task) { return; } if (!m_editTimeDialog) { qWarning() << "m_editTimeDialog is null"; return; } m_editTimeDialog->setProperty("taskUid", task->uid()); m_editTimeDialog->setProperty("taskName", task->name()); m_editTimeDialog->setProperty("taskDescription", task->description()); m_editTimeDialog->setProperty("minutes", static_cast(task->time())); m_editTimeDialog->setProperty("visible", true); } +void TaskView::editHistory() +{ + auto *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."), i18n( "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(int minutes) { addTimeToActiveTasks(-minutes, false); // subtract time in memory, but do not store it } 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::reconfigure() { /* Adapt columns */ m_tasksWidget->setColumnHidden(1, !KTimeTrackerSettings::displaySessionTime()); m_tasksWidget->setColumnHidden(2, !KTimeTrackerSettings::displayTime()); m_tasksWidget->setColumnHidden(3, !KTimeTrackerSettings::displayTotalSessionTime()); m_tasksWidget->setColumnHidden(4, !KTimeTrackerSettings::displayTotalTime()); m_tasksWidget->setColumnHidden(5, !KTimeTrackerSettings::displayPriority()); m_tasksWidget->setColumnHidden(6, !KTimeTrackerSettings::displayPercentComplete()); /* 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(); } 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, int 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 *deletedTask = static_cast(item); // Handle task deletion DesktopList desktopList; m_desktopTracker->registerForDesktops(deletedTask, desktopList); m_activeTasks.removeAll(deletedTask); emit tasksChanged(m_activeTasks); } diff --git a/src/taskview.h b/src/taskview.h index ed29818..a27137a 100644 --- a/src/taskview.h +++ b/src/taskview.h @@ -1,237 +1,239 @@ /* * 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(); /** Deliver if all events from the storage have and end time. */ bool allEventsHaveEndTiMe(); /** 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& taskame, const QString& taskdescription = QString(), long total = 0, long 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 refresh(); /** 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(int 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 reconfigure(); /** 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, int 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; QObject *m_editTimeDialog; private: void addTimeToActiveTasks(int minutes, bool save_data = true); /** item state stores if a task is expanded so you can see the subtasks */ 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