diff --git a/CMakeLists.txt b/CMakeLists.txt index 152d654..5c4dbe3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,51 +1,52 @@ cmake_minimum_required(VERSION 3.5) set(KDEPIM_VERSION_NUMBER "5.10.80") project(ktimetracker VERSION ${KDEPIM_VERSION_NUMBER}) set(KF5_MIN_VERSION "5.54.0") find_package(ECM ${KF5_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(ECMAddAppIcon) include(ECMInstallIcons) include(ECMQtDeclareLoggingCategory) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) ecm_setup_version(${KDEPIM_VERSION_NUMBER} VARIABLE_PREFIX KTIMETRACKER VERSION_HEADER src/ktimetracker-version.h ) set(QT_REQUIRED_VERSION "5.10.0") -find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED DBus Gui Widgets Xml Quick) +find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED DBus Gui Widgets Xml) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Config ConfigWidgets DBusAddons DocTools I18n IdleTime JobWidgets KIO Notifications WindowSystem XmlGui + TextWidgets ) find_package(KF5 5.63.0 REQUIRED COMPONENTS CalendarCore ) add_subdirectory(pics) add_subdirectory(icons) add_subdirectory(doc) add_subdirectory(src) install(FILES org.kde.ktimetracker.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7e223af..5b80482 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,96 +1,98 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) set(ktimetracker_SRCS + dialogs/edittimedialog.cpp + export/totalsastext.cpp export/csvhistory.cpp export/csvtotals.cpp export/export.cpp file/filecalendar.cpp file/icalformatkio.cpp model/event.cpp model/eventsmodel.cpp model/projectmodel.cpp model/task.cpp model/tasksmodel.cpp model/tasksmodelitem.cpp settings/ktimetrackerconfigdialog.cpp widgets/searchline.cpp widgets/taskswidget.cpp csvexportdialog.cpp desktoptracker.cpp edittaskdialog.cpp focusdetector.cpp historydialog.cpp idletimedetector.cpp ktimetrackerutility.cpp mainwindow.cpp import/plannerparser.cpp taskview.cpp timetrackerstorage.cpp timetrackerwidget.cpp tray.cpp treeviewheadercontextmenu.cpp $ $ ) ecm_qt_declare_logging_category(ktimetracker_SRCS HEADER ktt_debug.h IDENTIFIER KTT_LOG CATEGORY_NAME log_ktt ) qt5_add_dbus_adaptor(ktimetracker_SRCS org.kde.ktimetracker.ktimetracker.xml timetrackerwidget.h TimeTrackerWidget mainadaptor MainAdaptor ) ki18n_wrap_ui(ktimetracker_SRCS csvexportdialog.ui historydialog.ui edittaskdialog.ui settings/cfgbehavior.ui settings/cfgdisplay.ui settings/cfgstorage.ui ) kconfig_add_kcfg_files(ktimetracker_SRCS settings/ktimetracker.kcfgc) qt5_add_resources(ktimetracker_SRCS ktimetracker.qrc) add_library(libktimetracker STATIC ${ktimetracker_SRCS}) target_link_libraries(libktimetracker - Qt5::Quick KF5::ConfigWidgets KF5::WindowSystem KF5::Notifications KF5::I18n KF5::XmlGui KF5::JobWidgets KF5::KIOCore KF5::IdleTime KF5::DBusAddons KF5::CalendarCore + KF5::TextWidgets ) add_executable(ktimetracker main.cpp) target_link_libraries(ktimetracker libktimetracker) install(TARGETS ktimetracker ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES org.kde.ktimetracker.ktimetracker.xml DESTINATION ${DBUS_INTERFACES_INSTALL_DIR}) install(PROGRAMS ktimetracker.desktop DESTINATION ${KDE_INSTALL_APPDIR}) if(BUILD_TESTING) add_subdirectory(tests) endif() diff --git a/src/dialogs/edittimedialog.cpp b/src/dialogs/edittimedialog.cpp new file mode 100644 index 0000000..9cf7088 --- /dev/null +++ b/src/dialogs/edittimedialog.cpp @@ -0,0 +1,123 @@ +/* + * 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 "edittimedialog.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "ktimetrackerutility.h" + +EditTimeDialog::EditTimeDialog( + QWidget *parent, + const QString &name, const QString &description, + const int minutes) + : QDialog(parent), + m_initialMinutes(minutes), + m_editHistoryRequested(false) +{ + setWindowTitle(i18nc("@title:window", "Edit Task Time")); + setModal(true); + setMinimumSize(500, 330); + + auto *mainLayout = new QVBoxLayout(this); + setLayout(mainLayout); + + // Info group + auto *infoGroup = new QGroupBox(i18nc("@title:group", "Task"), this); + auto *infoLayout = new QGridLayout(infoGroup); + infoGroup->setLayout(infoLayout); + + QPalette roPalette; + roPalette.setColor(QPalette::Base, palette().color(QPalette::Background)); + + auto *nameText = new QLineEdit(name, infoGroup); + nameText->setReadOnly(true); + nameText->setPalette(roPalette); + infoLayout->addWidget(nameText, 0, 1); + infoLayout->addWidget(new QLabel(i18n("Task Name:")), 0, 0); + + auto *descText = new QPlainTextEdit(description, infoGroup); + descText->setReadOnly(true); + descText->setPalette(roPalette); + descText->setWordWrapMode(QTextOption::WordWrap); + infoLayout->addWidget(descText, 1, 1); + infoLayout->addWidget(new QLabel(i18n("Task Description:")), 1, 0); + + // Edit group + auto *editGroup = new QGroupBox(i18nc("@title:group", "Time Editing"), this); + auto *editLayout = new QGridLayout(editGroup); + editGroup->setLayout(editLayout); + + editLayout->setColumnStretch(0, 1); + editLayout->setColumnStretch(4, 1); + + editLayout->addWidget(new QLabel(formatTime(minutes)), 0, 2); // TODO increment over time while dialog is open + editLayout->addWidget(new QLabel(i18n("Current Time:")), 0, 1); + + m_timeEditor = new KPluralHandlingSpinBox(editGroup); + m_timeEditor->setSuffix(ki18ncp("@item:valuesuffix", " minute", " minutes")); + m_timeEditor->setRange(-24 * 3600, 24 * 3600); + m_timeEditor->setMinimumWidth(200); + m_timeEditor->setFocus(); + connect(m_timeEditor, QOverload::of(&QSpinBox::valueChanged), this, &EditTimeDialog::update); + editLayout->addWidget(m_timeEditor, 1, 2); + editLayout->addWidget(new QLabel(i18n("Change Time By:")), 1, 1); + + m_timePreview = new QLabel(editGroup); + editLayout->addWidget(m_timePreview, 2, 2); // TODO increment over time while dialog is open + editLayout->addWidget(new QLabel(i18n("Time After Change:")), 2, 1); + + m_buttonBox = new QDialogButtonBox(this); + m_buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + auto *historyButton = new QPushButton(QIcon::fromTheme("document-edit"), i18n("Edit History..."), m_buttonBox); + historyButton->setToolTip(i18n("To change this task's time, you have to edit its event history")); + m_buttonBox->addButton(historyButton, QDialogButtonBox::HelpRole); + + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &EditTimeDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::helpRequested, [=]() { + m_editHistoryRequested = true; + accept(); + }); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &EditTimeDialog::reject); + + mainLayout->addWidget(infoGroup); + mainLayout->addWidget(editGroup); + mainLayout->addStretch(1); + mainLayout->addWidget(m_buttonBox); + + update(0); +} + +void EditTimeDialog::update(int newValue) +{ + m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(newValue != 0); + m_timePreview->setText(formatTime(m_initialMinutes + newValue)); + m_changeMinutes = newValue; +} diff --git a/src/dialogs/edittimedialog.h b/src/dialogs/edittimedialog.h new file mode 100644 index 0000000..86fb451 --- /dev/null +++ b/src/dialogs/edittimedialog.h @@ -0,0 +1,56 @@ +/* + * 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_EDITTIMEDIALOG_H +#define KTIMETRACKER_EDITTIMEDIALOG_H + +#include + +class KPluralHandlingSpinBox; +class QLabel; +class QDialogButtonBox; + +class EditTimeDialog : public QDialog +{ + Q_OBJECT + +public: + explicit EditTimeDialog( + QWidget *parent, + const QString &name, const QString &description, + int minutes); + ~EditTimeDialog() override = default; + + int changeMinutes() const { return m_changeMinutes; } + bool editHistoryRequested() const { return m_editHistoryRequested; } + +protected Q_SLOTS: + void update(int newValue); + +private: + QDialogButtonBox *m_buttonBox; + KPluralHandlingSpinBox *m_timeEditor; + QLabel *m_timePreview; + int m_initialMinutes; + int m_changeMinutes; + bool m_editHistoryRequested; +}; + +#endif // KTIMETRACKER_EDITTIMEDIALOG_H diff --git a/src/ktimetracker.qrc b/src/ktimetracker.qrc index ce1398f..8914c45 100644 --- a/src/ktimetracker.qrc +++ b/src/ktimetracker.qrc @@ -1,8 +1,5 @@ ktimetrackerui.rc - - qml/EditTimeDialog.qml - diff --git a/src/qml/EditTimeDialog.qml b/src/qml/EditTimeDialog.qml deleted file mode 100644 index 8f3829b..0000000 --- a/src/qml/EditTimeDialog.qml +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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 - - GridLayout { - columns: 2 - - Text { - text: i18n("Task Name:") - } - - Text { - text: taskName - - font.bold: true - wrapMode: Text.Wrap - } - - Text { - text: i18n("Task Description:") - } - - Text { - text: taskDescription - - Layout.fillWidth: true - wrapMode: Text.Wrap - elide: Text.ElideRight - maximumLineCount: 5 - } - - Rectangle { - height: 1 - color: Qt.tint(Kirigami.Theme.textColor, Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.7)) - - Layout.columnSpan: 2 - Layout.fillWidth: true - Layout.topMargin: 10 - Layout.bottomMargin: 10 - } - - Text { - text: i18n("Current Time:") - } - - Text { - text: formatTime(minutes) - } - - Text { - text: i18n("Change Time By:") - } - - 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") - } - } - - Text { - text: i18n("Time After Change:") - } - - Text { - text: formatTime(minutes + changeMinutes) - } - } - - Item { - Layout.fillHeight: true - } - - RowLayout { - Button { - 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(); - } - } - - Item { - Layout.fillWidth: true - } - - 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() - } - } - - 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 d4535c8..2ca8c91 100644 --- a/src/taskview.cpp +++ b/src/taskview.cpp @@ -1,812 +1,802 @@ /* * 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 "dialogs/edittimedialog.h" #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; + 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()); + } } - 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); + delete editTimeDialog; } void TaskView::editHistory() { - auto *dialog = new HistoryDialog(m_tasksWidget->parentWidget(), storage()->projectModel()); + 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."), 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); }